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 */}
{/* 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
| Vendedor |
Tienda |
Nº Ventas |
Ventas Sin Impuesto |
Impuesto (15%) |
Ventas Con Impuesto |
Meta Mensual |
% Cumplimiento |
{vendedorPerformance.map(item => (
|
{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
| # ID |
Fecha |
Tienda |
Vendedor |
Concepto / Producto |
Subtotal (Sin ISV) |
ISV 15% |
Total Con Impuesto |
{filteredSales.map((sale) => (
| #{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 })}
|
))}
{filteredSales.length === 0 && (
|
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 => (
))}
);
})}
)}
{}
{isAddModalOpen && (
Registrar Nueva Venta
Ingresa los datos correspondientes a la transacción de venta
)}
{}
{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
)}
);
}