/* app.jsx — App shell with auth, API, and AI generation */ /* ── API helpers ───────────────────────────────────────────────────────────── */ async function apiFetch(url, opts = {}) { const res = await fetch(url, { credentials: 'same-origin', ...opts }); if (res.status === 401) { window.location.href = '/login.html'; throw new Error('unauth'); } return res; } /* ── Debounce ──────────────────────────────────────────────────────────────── */ function useDebounce(fn, delay) { const timerRef = React.useRef(null); return React.useCallback((...args) => { clearTimeout(timerRef.current); timerRef.current = setTimeout(() => fn(...args), delay); }, [fn, delay]); } /* ── GenerateModal ─────────────────────────────────────────────────────────── */ const TIPOS = [ 'Landing page', 'Sitio web corporativo', 'Tienda en línea', 'Estrategia SEO', 'Google Ads', 'Paquete combinado', 'Otro', ]; function GenerateModal({ onClose, onCreated }) { const [form, setForm] = React.useState({ empresa: '', contacto: '', email: '', tipo: '', descripcion: '', presupuesto: '' }); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(''); function set(k, v) { setForm(f => ({ ...f, [k]: v })); } async function handleSubmit(e) { e.preventDefault(); if (!form.empresa || !form.tipo || !form.descripcion) { setError('Empresa, tipo y descripción son requeridos.'); return; } setError(''); setLoading(true); try { const genRes = await apiFetch('/api/generate.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), }); if (!genRes.ok) { const d = await genRes.json(); setError(d.error || 'Error al generar.'); setLoading(false); return; } const { data } = await genRes.json(); const today = new Date(); const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; const validUntil = new Date(today); validUntil.setDate(validUntil.getDate() + 30); const quote = { ...data, status: 'draft', createdAt: fmt(today), validUntil: fmt(validUntil) }; const saveRes = await apiFetch('/api/quotes.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(quote), }); if (!saveRes.ok) { setError('Error al guardar la cotización.'); setLoading(false); return; } const { id, token } = await saveRes.json(); onCreated({ ...quote, id, token }); } catch (err) { if (err.message !== 'unauth') setError('Error de conexión. Intenta de nuevo.'); setLoading(false); } } const labelStyle = { display: 'block', fontSize: 11, fontFamily: 'Poppins', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '1.5px', color: 'rgba(255,255,255,0.45)', marginBottom: 6 }; const inputStyle = { width: '100%', padding: '10px 14px', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 10, color: '#fff', fontSize: 14, fontFamily: 'inherit', outline: 'none' }; return (
e.target === e.currentTarget && onClose()}>
Generar con IA
Claude redactará la propuesta automáticamente
set('empresa', e.target.value)} placeholder="Nombre del cliente" required />
set('contacto', e.target.value)} placeholder="Nombre y apellido" />
set('email', e.target.value)} placeholder="correo@empresa.com" />