import React, { useState, useMemo, useEffect } from ‘react’; const STORES_CONFIG = { ‘CM’: { name: ‘Tienda CM’, color: ‘#3B82F6’, // Blue vendedores: [‘David’, ‘Antonio’, ‘Josué’, ‘Erlin’] }, ‘OB’: { name: ‘Tienda OB’, color: ‘#10B981’, // Emerald vendedores: [‘Noé’, ‘Renán’, ‘Walter’, ‘Jonathan’] }, ‘CH’: { name: ‘Tienda CH’, color: ‘#8B5CF6’, // Purple vendedores: [‘Samuel’, ‘Wilson’, ‘Erick’] } }; const DEFAULT_TAX_RATE = 0.15; // 15% ISV tax const generateSampleData = () => { const sales = []; const currentDate = new Date(); const year = currentDate.getFullYear(); const month = currentDate.getMonth(); // Current month const daysInMonth = new Date(year, month + 1, 0).getDate(); let idCounter = 1; // Generate realistic sales for each store & salesperson across the days Object.keys(STORES_CONFIG).forEach(storeKey => { const vendedores = STORES_CONFIG[storeKey].vendedores; vendedores.forEach(vendedor => { // 10-20 transactions per salesperson in the month const numTransactions = Math.floor(Math.random() * 8) + 12; for (let i = 0; i < numTransactions; i++) { const day = Math.floor(Math.random() * daysInMonth) + 1; const formattedDay = String(day).padStart(2, '0'); const formattedMonth = String(month + 1).padStart(2, '0'); const dateStr = `${year}-${formattedMonth}-${formattedDay}`; // Subtotal between 1,200 and 18,500 Lempiras/USD const subtotal = Math.round((Math.random() * 15000 + 1200) * 100) / 100; const tax = Math.round((subtotal * DEFAULT_TAX_RATE) * 100) / 100; const total = Math.round((subtotal + tax) * 100) / 100; sales.push({ id: idCounter++, fecha: dateStr, tienda: storeKey, vendedor: vendedor, concepto: `Venta de Repuestos #${1000 + idCounter}`, subtotal: subtotal, impuesto: tax, total: total }); } }); }); return sales.sort((a, b) => new Date(b.fecha) – new Date(a.fecha)); }; // Initial target projections per salesperson const initialProjections = { // CM ‘David’: 140000, ‘Antonio’: 125000, ‘Josué’: 110000, ‘Erlin’: 115000, // OB ‘Noé’: 130000, ‘Renán’: 120000, ‘Walter’: 110000, ‘Jonathan’: 105000, // CH ‘Samuel’: 135000, ‘Wilson’: 125000, ‘Erick’: 120000 }; const IconStore = () => ( ); const IconUser = () => ( ); const IconTrendingUp = () => ( ); const IconDollar = () => ( ); const IconPlus = () => ( ); const IconUpload = () => ( ); const IconDownload = () => ( ); const IconFilter = () => ( ); export default function App() { // Main sales state const [salesData, setSalesData] = useState(() => generateSampleData()); const [projections, setProjections] = useState(initialProjections); // Filter States const [selectedStore, setSelectedStore] = useState(‘ALL’); const [selectedVendedor, setSelectedVendedor] = useState(‘ALL’); const [dateView, setDateView] = useState(‘MONTH’); // ‘MONTH’, ‘WEEK’, ‘DAY’, ‘CUSTOM’ const currentDate = new Date(); const currentMonthStr = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, ‘0’)}`; const [selectedMonth, setSelectedMonth] = useState(currentMonthStr); const [selectedDate, setSelectedDate] = useState(currentDate.toISOString().split(‘T’)[0]); const [activeTab, setActiveTab] = useState(‘dashboard’); // ‘dashboard’, ‘sales’, ‘projections’ // Modals const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); const [toastMessage, setToastMessage] = useState(null); // New Sale Form State const [newSale, setNewSale] = useState({ fecha: currentDate.toISOString().split(‘T’)[0], tienda: ‘CM’, vendedor: ‘David’, concepto: », subtotal: », incluyeImpuesto: false }); // Display Toast helper const showToast = (msg, type = ‘success’) => { setToastMessage({ text: msg, type }); setTimeout(() => setToastMessage(null), 3500); }; const filteredSales = useMemo(() => { return salesData.filter(sale => { // Store Filter if (selectedStore !== ‘ALL’ && sale.tienda !== selectedStore) return false; // Vendedor Filter if (selectedVendedor !== ‘ALL’ && sale.vendedor !== selectedVendedor) return false; // Time Filter logic if (dateView === ‘MONTH’) { return sale.fecha.startsWith(selectedMonth); } else if (dateView === ‘DAY’) { return sale.fecha === selectedDate; } else if (dateView === ‘WEEK’) { // Calculate week range from selectedDate const sel = new Date(selectedDate); const dayOfWeek = sel.getDay(); const startOfWeek = new Date(sel); startOfWeek.setDate(sel.getDate() – dayOfWeek); const endOfWeek = new Date(startOfWeek); endOfWeek.setDate(startOfWeek.getDate() + 6); const saleDate = new Date(sale.fecha); return saleDate >= startOfWeek && saleDate <= endOfWeek; } return true; }); }, [salesData, selectedStore, selectedVendedor, dateView, selectedMonth, selectedDate]); const metrics = useMemo(() => { const totalSubtotal = filteredSales.reduce((acc, s) => acc + s.subtotal, 0); const totalImpuesto = filteredSales.reduce((acc, s) => acc + s.impuesto, 0); const totalVentas = filteredSales.reduce((acc, s) => acc + s.total, 0); // Calculate total projection target based on current selection let totalTarget = 0; Object.keys(STORES_CONFIG).forEach(storeKey => { if (selectedStore === ‘ALL’ || selectedStore === storeKey) { STORES_CONFIG[storeKey].vendedores.forEach(vendedor => { if (selectedVendedor === ‘ALL’ || selectedVendedor === vendedor) { totalTarget += (projections[vendedor] || 0); } }); } }); const percentAchieved = totalTarget > 0 ? (totalSubtotal / totalTarget) * 100 : 0; return { subtotal: totalSubtotal, impuesto: totalImpuesto, total: totalVentas, target: totalTarget, cumplimiento: percentAchieved, count: filteredSales.length }; }, [filteredSales, selectedStore, selectedVendedor, projections]); // Daily Sales grouping const dailySalesData = useMemo(() => { const daysMap = {}; // Sort transactions chronologically const sorted = […filteredSales].sort((a, b) => new Date(a.fecha) – new Date(b.fecha)); sorted.forEach(sale => { const dayLabel = sale.fecha.split(‘-‘).slice(1).join(‘/’); // MM/DD if (!daysMap[dayLabel]) { daysMap[dayLabel] = { day: dayLabel, subtotal: 0, total: 0, count: 0 }; } daysMap[dayLabel].subtotal += sale.subtotal; daysMap[dayLabel].total += sale.total; daysMap[dayLabel].count += 1; }); return Object.values(daysMap); }, [filteredSales]); // Vendedor Sales grouping const vendedorPerformance = useMemo(() => { const list = []; Object.keys(STORES_CONFIG).forEach(storeKey => { if (selectedStore !== ‘ALL’ && selectedStore !== storeKey) return; STORES_CONFIG[storeKey].vendedores.forEach(vendedor => { if (selectedVendedor !== ‘ALL’ && selectedVendedor !== vendedor) return; const vSales = filteredSales.filter(s => s.vendedor === vendedor); const subtotal = vSales.reduce((acc, s) => acc + s.subtotal, 0); const total = vSales.reduce((acc, s) => acc + s.total, 0); const target = projections[vendedor] || 0; const pct = target > 0 ? (subtotal / target) * 100 : 0; list.push({ vendedor, tienda: storeKey, subtotal, total, target, pct, count: vSales.length }); }); }); return list.sort((a, b) => b.subtotal – a.subtotal); }, [filteredSales, selectedStore, selectedVendedor, projections]); // Store performance summary const storePerformance = useMemo(() => { return Object.keys(STORES_CONFIG).map(storeKey => { const storeSales = salesData.filter(s => s.tienda === storeKey && s.fecha.startsWith(selectedMonth)); const subtotal = storeSales.reduce((acc, s) => acc + s.subtotal, 0); const total = storeSales.reduce((acc, s) => acc + s.total, 0); let storeTarget = 0; STORES_CONFIG[storeKey].vendedores.forEach(v => { storeTarget += projections[v] || 0; }); const pct = storeTarget > 0 ? (subtotal / storeTarget) * 100 : 0; return { key: storeKey, name: STORES_CONFIG[storeKey].name, color: STORES_CONFIG[storeKey].color, subtotal, total, target: storeTarget, pct }; }); }, [salesData, selectedMonth, projections]); // Download CSV / Excel Template const handleDownloadTemplate = () => { const csvContent = «data:text/csv;charset=utf-8,» + «Fecha,Tienda,Vendedor,Concepto,Subtotal_Sin_Impuesto\n» + `${currentDate.toISOString().split(‘T’)[0]},CM,David,Venta Baterías LTH,4500.00\n` + `${currentDate.toISOString().split(‘T’)[0]},OB,Noé,Aceite Castrol 20W50,2800.00\n` + `${currentDate.toISOString().split(‘T’)[0]},CH,Samuel,Juego Llantas 205/55R16,9200.00`; const encodedUri = encodeURI(csvContent); const link = document.createElement(«a»); link.setAttribute(«href», encodedUri); link.setAttribute(«download», «Plantilla_Ventas_RecarAutomotriz.csv»); document.body.appendChild(link); link.click(); document.body.removeChild(link); showToast(«Plantilla descargada con éxito»); }; // Export Current Data to CSV const handleExportData = () => { let csv = «ID,Fecha,Tienda,Vendedor,Concepto,Subtotal (Sin ISV),Impuesto (15%),Total (Con ISV)\n»; filteredSales.forEach(s => { csv += `»${s.id}»,»${s.fecha}»,»${s.tienda}»,»${s.vendedor}»,»${s.concepto.replace(/»/g, ‘»»‘)}»,${s.subtotal.toFixed(2)},${s.impuesto.toFixed(2)},${s.total.toFixed(2)}\n`; }); const blob = new Blob([csv], { type: ‘text/csv;charset=utf-8;’ }); const url = URL.createObjectURL(blob); const link = document.createElement(‘a’); link.setAttribute(‘href’, url); link.setAttribute(‘download’, `Ventas_RecarAutomotriz_${selectedMonth}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); showToast(«Reporte exportado correctamente»); }; // CSV File Import Parser const handleFileUpload = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (evt) => { try { const text = evt.target.result; const lines = text.split(‘\n’); const newEntries = []; let idCounter = salesData.length + 100; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const parts = line.split(','); if (parts.length >= 5) { const fecha = parts[0].replace(/»/g, »).trim(); const tienda = parts[1].replace(/»/g, »).trim().toUpperCase(); const vendedor = parts[2].replace(/»/g, »).trim(); const concepto = parts[3].replace(/»/g, »).trim(); const val = parseFloat(parts[4].replace(/»/g, »).trim()); if (fecha && STORES_CONFIG[tienda] && !isNaN(val)) { const subtotal = Math.round(val * 100) / 100; const tax = Math.round((subtotal * DEFAULT_TAX_RATE) * 100) / 100; newEntries.push({ id: idCounter++, fecha, tienda, vendedor, concepto: concepto || ‘Venta Importada Excel’, subtotal, impuesto: tax, total: subtotal + tax }); } } } if (newEntries.length > 0) { setSalesData(prev => […newEntries, …prev]); setIsUploadModalOpen(false); showToast(`¡Se importaron ${newEntries.length} ventas desde el archivo!`); } else { showToast(«No se encontraron registros válidos. Verifica el formato del archivo.», «error»); } } catch (err) { showToast(«Error al procesar el archivo Excel/CSV», «error»); } }; reader.readAsText(file); }; // Handle New Manual Sale const handleAddSaleSubmit = (e) => { e.preventDefault(); const val = parseFloat(newSale.subtotal); if (!val || val <= 0) { showToast("Ingresa un monto válido", "error"); return; } let subtotal, tax, total; if (newSale.incluyeImpuesto) { total = val; subtotal = Math.round((total / (1 + DEFAULT_TAX_RATE)) * 100) / 100; tax = Math.round((total - subtotal) * 100) / 100; } else { subtotal = val; tax = Math.round((subtotal * DEFAULT_TAX_RATE) * 100) / 100; total = subtotal + tax; } const createdSale = { id: Date.now(), fecha: newSale.fecha, tienda: newSale.tienda, vendedor: newSale.vendedor, concepto: newSale.concepto || 'Venta Mostrador', subtotal, impuesto: tax, total }; setSalesData([createdSale, ...salesData]); setIsAddModalOpen(false); showToast("Venta registrada exitosamente"); // Reset Form setNewSale({ fecha: currentDate.toISOString().split('T')[0], tienda: 'CM', vendedor: 'David', concepto: '', subtotal: '', incluyeImpuesto: false }); }; // Available vendedores dynamically based on active store filter const availableVendedores = useMemo(() => { if (selectedStore === ‘ALL’) { return Object.values(STORES_CONFIG).flatMap(s => s.vendedores); } return STORES_CONFIG[selectedStore]?.vendedores || []; }, [selectedStore]); return (
{/* Toast Notification */} {toastMessage && (
{toastMessage.text}
)} {/* Header Bar */}
RA

RECAR AUTOMOTRIZ

Dashboard de Ventas & Proyecciones Mensuales

{/* Quick Actions */}
{/* Main Container */}
{/* Navigation Tabs */}
{/* Dynamic Filters Bar */}
{/* Store Filter Buttons */}
Tienda: {Object.keys(STORES_CONFIG).map(storeKey => ( ))}
{/* Sub-Filters */}
{/* Salesperson Selector */}
{/* Date Mode Selector */}
{/* Dynamic Date Pickers */} {dateView === ‘MONTH’ && ( setSelectedMonth(e.target.value)} className=»bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-500″ /> )} {(dateView === ‘DAY’ || dateView === ‘WEEK’) && ( setSelectedDate(e.target.value)} className=»bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-500″ /> )}
{} {activeTab === ‘dashboard’ && (
{/* KPI Cards Grid */}
{/* Card 1: Ventas Sin Impuesto */}

Ventas Sin Impuesto

L. {metrics.subtotal.toLocaleString(‘es-HN’, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

Subtotal acumulado en el periodo

{/* Card 2: Impuesto Recaudado (15%) */}

Impuesto ISV (15%)

L. {metrics.impuesto.toLocaleString(‘es-HN’, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

Impuesto sobre ventas abonado

{/* Card 3: Ventas Con Impuesto */}

Total Con Impuesto

L. {metrics.total.toLocaleString(‘es-HN’, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

{metrics.count} transacciones realizadas

{/* Card 4: Proyección & % Cumplimiento */}

Proyección / Meta

= 100 ? ‘bg-emerald-500/20 text-emerald-400 border border-emerald-500/30’ : metrics.cumplimiento >= 75 ? ‘bg-blue-500/20 text-blue-400 border border-blue-500/30’ : ‘bg-amber-500/20 text-amber-400 border border-amber-500/30’ }`}> {metrics.cumplimiento.toFixed(1)}% Meta

L. {metrics.target.toLocaleString(‘es-HN’)}

{/* Progress Bar */}
= 100 ? ‘bg-emerald-500’ : metrics.cumplimiento >= 75 ? ‘bg-blue-500’ : ‘bg-amber-500’ }`} style={{ width: `${Math.min(metrics.cumplimiento, 100)}%` }} >
{/* Charts Section */}
{/* Daily Sales Bar / Trend Chart */}

Ventas Diarias del Mes

Evolución de subtotal diario acumulado

{dailySalesData.length} días con registros
{/* SVG Custom Daily Chart */} {dailySalesData.length > 0 ? (
{dailySalesData.map((d, idx) => { const maxVal = Math.max(…dailySalesData.map(item => item.subtotal), 1); const heightPct = Math.max((d.subtotal / maxVal) * 100, 4); return (
{/* Hover Tooltip */}

{d.day}

Subtotal: L. {d.subtotal.toLocaleString(‘es-HN’)}

Total: L. {d.total.toLocaleString(‘es-HN’)}

{/* Bar Visual */}
{d.day.split(‘/’)[1]}
); })}
) : (
No hay datos de ventas en las fechas seleccionadas
)}
Subtotal Sin ISV
Promedio Diario: L. {(metrics.subtotal / (dailySalesData.length || 1)).toLocaleString(‘es-HN’, { maximumFractionDigits: 0 })}
{/* Store Distribution Breakdown */}

Rendimiento por Tienda

Proyección mensual {selectedMonth}

{storePerformance.map(store => (
{store.name} {store.pct.toFixed(1)}%
Vendido: L. {store.subtotal.toLocaleString(‘es-HN’)} Meta: L. {store.target.toLocaleString(‘es-HN’)}
))}
Tiendas operativas: 3 Impuesto General: 15% ISV
{/* Salesperson Leaderboard Section */}

Desempeño de Vendedores

Comparativa de ventas reales vs meta asignada por vendedor

{vendedorPerformance.length} Vendedores Evaluados
{vendedorPerformance.map(item => ( ))}
Vendedor Tienda Nº Ventas Ventas Sin Impuesto Impuesto (15%) Ventas Con Impuesto Meta Mensual % Cumplimiento
{item.vendedor[0]}
{item.vendedor}
{item.tienda} {item.count} L. {item.subtotal.toLocaleString(‘es-HN’, { minimumFractionDigits: 2 })} L. {(item.subtotal * DEFAULT_TAX_RATE).toLocaleString(‘es-HN’, { minimumFractionDigits: 2 })} L. {item.total.toLocaleString(‘es-HN’, { minimumFractionDigits: 2 })} L. {item.target.toLocaleString(‘es-HN’)} = 100 ? ‘bg-emerald-500/20 text-emerald-400’ : item.pct >= 75 ? ‘bg-blue-500/20 text-blue-400’ : ‘bg-amber-500/20 text-amber-400’ }`}> {item.pct.toFixed(1)}%
)} {} {activeTab === ‘sales’ && (

Historial de Transacciones de Venta

Listado detallado con desglose de subtotal e impuesto ISV (15%)

Mostrando {filteredSales.length} registros
{filteredSales.map((sale) => ( ))} {filteredSales.length === 0 && ( )}
# ID Fecha Tienda Vendedor Concepto / Producto Subtotal (Sin ISV) ISV 15% Total Con Impuesto
#{sale.id} {sale.fecha} {sale.tienda} {sale.vendedor} {sale.concepto} L. {sale.subtotal.toLocaleString(‘es-HN’, { minimumFractionDigits: 2 })} L. {sale.impuesto.toLocaleString(‘es-HN’, { minimumFractionDigits: 2 })} L. {sale.total.toLocaleString(‘es-HN’, { minimumFractionDigits: 2 })}
No se encontraron registros de venta con los filtros seleccionados.
)} {} {activeTab === ‘projections’ && (

Gestión de Proyecciones Mensuales

Ajusta las metas de venta individuales por tienda y vendedor

{Object.keys(STORES_CONFIG).map(storeKey => { const store = STORES_CONFIG[storeKey]; const storeTotalTarget = store.vendedores.reduce((acc, v) => acc + (projections[v] || 0), 0); return (

{store.name}

{store.vendedores.length} Vendedores

Meta Tienda

L. {storeTotalTarget.toLocaleString(‘es-HN’)}

{store.vendedores.map(vendedor => (
L. { const val = parseFloat(e.target.value) || 0; setProjections(prev => ({ …prev, [vendedor]: val })); }} className=»w-full bg-slate-900 border border-slate-700 rounded-lg pl-8 pr-3 py-1.5 text-xs text-white font-mono focus:outline-none focus:ring-2 focus:ring-blue-500″ />
))}
); })}
)}
{} {isAddModalOpen && (

Registrar Nueva Venta

Ingresa los datos correspondientes a la transacción de venta

setNewSale({ …newSale, fecha: e.target.value })} className=»w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-white focus:outline-none focus:ring-2 focus:ring-blue-500″ />
setNewSale({ …newSale, subtotal: e.target.value })} className=»w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-white font-mono focus:outline-none focus:ring-2 focus:ring-blue-500″ />
setNewSale({ …newSale, concepto: e.target.value })} className=»w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-white focus:outline-none focus:ring-2 focus:ring-blue-500″ />
setNewSale({ …newSale, incluyeImpuesto: e.target.checked })} className=»w-4 h-4 text-blue-600 rounded bg-slate-800 border-slate-700 focus:ring-blue-500″ />
{/* Live calculation preview */} {newSale.subtotal && (
Subtotal Sin ISV: L. {( newSale.incluyeImpuesto ? parseFloat(newSale.subtotal || 0) / 1.15 : parseFloat(newSale.subtotal || 0) ).toFixed(2)}
Impuesto ISV (15%): L. {( newSale.incluyeImpuesto ? parseFloat(newSale.subtotal || 0) – (parseFloat(newSale.subtotal || 0) / 1.15) : parseFloat(newSale.subtotal || 0) * DEFAULT_TAX_RATE ).toFixed(2)}
Total a Pagar: L. {( newSale.incluyeImpuesto ? parseFloat(newSale.subtotal || 0) : parseFloat(newSale.subtotal || 0) * 1.15 ).toFixed(2)}
)}
)} {} {isUploadModalOpen && (

Cargar Archivo Excel / CSV

Sube tus hojas de cálculo de ventas para integrarlas al instante en el dashboard.

Haz clic para seleccionar o arrastra tu archivo CSV / Excel

Soporta columnas: Fecha, Tienda (CM, OB, CH), Vendedor, Concepto, Subtotal

Estructura esperada del Excel:

Fecha | Tienda | Vendedor | Concepto | Subtotal

)}
); }