/* views.jsx — Dashboard, Editor, Preview */ /* ============================== DASHBOARD ============================== */ function Dashboard({ quotes, openQuote, openPreview, newQuote, duplicateQuote, deleteQuote, onGenerate }) { const [filter, setFilter] = React.useState('all'); const [query, setQuery] = React.useState(''); const filtered = quotes.filter(q => { if (filter !== 'all' && q.status !== filter) return false; if (query) { const t = (q.client.company + ' ' + q.project.name + ' ' + q.id).toLowerCase(); if (!t.includes(query.toLowerCase())) return false; } return true; }); /* Stats */ const now = new Date(); const thisMonth = quotes.filter(q => { const d = new Date(q.createdAt); return d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear(); }); const monthTotal = thisMonth.reduce((s, q) => s + computeTotals(q).total, 0); const accepted = quotes.filter(q => q.status === 'accepted'); const acceptedTotal = accepted.reduce((s, q) => s + computeTotals(q).total, 0); const inReview = quotes.filter(q => q.status === 'sent').length; const decided = quotes.filter(q => q.status === 'accepted' || q.status === 'rejected'); const winRate = decided.length ? Math.round(100 * accepted.length / decided.length) : 0; const counts = { all: quotes.length, draft: quotes.filter(q => q.status === 'draft').length, sent: quotes.filter(q => q.status === 'sent').length, accepted: quotes.filter(q => q.status === 'accepted').length, rejected: quotes.filter(q => q.status === 'rejected').length, expired: quotes.filter(q => q.status === 'expired').length, }; return (
Panel principal

Cotizaciones

{quotes.length} cotizaciones en total · {inReview} esperando respuesta
{/* Stats */}
Facturado este mes
{fmtMXNshort(monthTotal)}
▲ 18% vs. abril
Aceptadas (YTD)
{fmtMXNshort(acceptedTotal)}
{accepted.length} proyectos firmados
En revisión
{inReview}
Vigencia promedio 22 días
Tasa de conversión
{winRate}%
▲ 6 pts vs. trimestre
{/* Table */}
setQuery(e.target.value)} />
{[ ['all', 'Todas'], ['draft', 'Borrador'], ['sent', 'Enviadas'], ['accepted', 'Aceptadas'], ['rejected', 'Rechazadas'], ['expired', 'Expiradas'], ].map(([k, l]) => ( ))}
{filtered.map(q => { const tot = computeTotals(q); return ( openQuote(q.id)}> ); })} {!filtered.length && ( )}
Folio Cliente / Proyecto Monto Estado Enviada Vigencia Acciones
{q.id} {q.client.company} {q.project.name} {fmtMXNshort(tot.total)} {fmtDateShort(q.createdAt)} {fmtDateShort(q.validUntil)} e.stopPropagation()}>
Sin resultados para los filtros aplicados.
); } /* ============================== EDITOR ============================== */ function Editor({ quote, updateQuote, openPreview, back }) { const [catalogOpen, setCatalogOpen] = React.useState(false); if (!quote) return null; const tot = computeTotals(quote); const patch = (path, value) => { updateQuote(quote.id, prev => { const next = JSON.parse(JSON.stringify(prev)); const segs = path.split('.'); let cur = next; for (let i = 0; i < segs.length - 1; i++) { if (cur[segs[i]] == null || typeof cur[segs[i]] !== 'object') cur[segs[i]] = {}; cur = cur[segs[i]]; } cur[segs[segs.length - 1]] = value; return next; }); }; const patchItem = (id, key, value) => { updateQuote(quote.id, prev => ({ ...prev, items: prev.items.map(it => it.id === id ? { ...it, [key]: value } : it), })); }; const removeItem = (id) => updateQuote(quote.id, prev => ({ ...prev, items: prev.items.filter(it => it.id !== id) })); const addFromCatalog = (entry) => { const newItem = { id: Date.now(), ref: entry.id, name: entry.name, sub: entry.sub, qty: 1, unit: entry.unit, price: entry.price, }; updateQuote(quote.id, prev => ({ ...prev, items: [...prev.items, newItem] })); setCatalogOpen(false); }; const addBlank = () => { updateQuote(quote.id, prev => ({ ...prev, items: [...prev.items, { id: Date.now(), name: 'Nuevo concepto', sub: '', qty: 1, unit: 'pieza', price: 0 }], })); }; const setSchedulePct = (idx, pct) => { updateQuote(quote.id, prev => ({ ...prev, schedule: prev.schedule.map((s, i) => i === idx ? { ...s, pct: Number(pct) || 0 } : s), })); }; return (
Editor de cotización · {quote.id}

{quote.project.name}

{quote.client.company} · Creada {fmtDateLong(quote.createdAt)}
{/* Cliente */}
Cliente
patch('client.company', e.target.value)} />
patch('client.contact', e.target.value)} />
patch('client.email', e.target.value)} />
patch('client.rfc', e.target.value)} />
patch('client.address', e.target.value)} />
{/* Proyecto */}
Proyecto
patch('project.name', e.target.value)} />
patch('project.type', e.target.value)} />