/* data.jsx — sample data, helpers, catalog */ const CATALOG = [ { id: 'landing', name: 'Landing Page', unit: 'pieza', price: 18000, sub: 'Página de alta conversión · Hasta 6 secciones · CMS opcional' }, { id: 'corp', name: 'Sitio web corporativo', unit: 'pieza', price: 35000, sub: '5 a 8 páginas · WordPress · SEO on-page incluido' }, { id: 'ecom', name: 'Tienda en línea', unit: 'pieza', price: 55000, sub: 'WooCommerce o Shopify · Hasta 100 SKUs · Pasarela de pago MX' }, { id: 'seo', name: 'Estrategia SEO mensual', unit: 'mes', price: 6500, sub: 'Auditoría, on-page, link building y reporte mensual' }, { id: 'ads-setup', name: 'Configuración Google Ads', unit: 'pieza', price: 8000, sub: 'Estructura de campañas, keywords, copies y conversiones' }, { id: 'ads-mgmt', name: 'Gestión Google Ads mensual', unit: 'mes', price: 5500, sub: 'Optimización, reportes y ajuste de pujas · No incluye presupuesto' }, { id: 'maint', name: 'Mantenimiento web mensual', unit: 'mes', price: 1800, sub: 'Actualizaciones, respaldos, soporte técnico y velocidad' }, { id: 'copy', name: 'Copywriting y textos del sitio', unit: 'pieza', price: 8000, sub: 'Hasta 6 secciones · Tono adaptado a la marca · SEO-friendly' }, ]; const STATUS_LABEL = { draft: 'Borrador', sent: 'Enviada', accepted: 'Aceptada', rejected: 'Rechazada', expired: 'Expirada', }; /* HELPERS */ const fmtMXN = (n) => '$' + (Math.round(n)).toLocaleString('es-MX') + ' MXN'; const fmtMXNshort = (n) => '$' + (Math.round(n)).toLocaleString('es-MX'); const fmtDateLong = (iso) => { if (!iso || iso === '—') return iso || '—'; const d = new Date(iso + 'T12:00:00'); return d.toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' }); }; const fmtDateShort = (iso) => { if (!iso) return ''; const d = new Date(iso + 'T12:00:00'); return d.toLocaleDateString('es-MX', { day: '2-digit', month: 'short' }); }; function computeTotals(quote) { const subtotal = quote.items.reduce((s, it) => s + (it.qty * it.price), 0); let discount = 0; if (quote.discount?.type === 'pct') discount = subtotal * (quote.discount.value / 100); if (quote.discount?.type === 'fixed') discount = quote.discount.value; const taxable = subtotal - discount; const iva = taxable * 0.16; const total = taxable + iva; return { subtotal, discount, taxable, iva, total }; } /* paymentSummary — produce a short "50% / 50%" or "40% / 30% / 30%" label */ function paymentSummary(quote) { return quote.schedule.map(s => `${s.pct}%`).join(' / '); } const LogoMark = ({ size = 26 }) => ( Source Code ); const Brand = () => (
Source Code
); /* SECTION LABEL — small uppercase tracked label */ const SectionLabel = ({ children, style }) => (
{children}
); const StatusPill = ({ status }) => ( {STATUS_LABEL[status] || status} ); /* small inline icons */ const Icon = { search: (p) => , plus: (p) => , back: (p) => , trash: (p) => , eye: (p) => , send: (p) => , copy: (p) => , dl: (p) => , chev: (p) => , drag: () => ::, check: (p) => , arrowR: (p) => , clock: (p) => , spark: (p) => , }; Object.assign(window, { CATALOG, STATUS_LABEL, fmtMXN, fmtMXNshort, fmtDateLong, fmtDateShort, computeTotals, paymentSummary, LogoMark, Brand, SectionLabel, StatusPill, Icon });