Spaces:
Running
Running
| import React, { useState, useEffect } from 'react'; | |
| import { db } from '../../firebase/config'; | |
| import { ref, onValue } from 'firebase/database'; | |
| import { | |
| Chart as ChartJS, CategoryScale, LinearScale, PointElement, | |
| LineElement, Title, Tooltip, Legend, BarElement | |
| } from 'chart.js'; | |
| import { Line, Bar } from 'react-chartjs-2'; | |
| import { TrendingUp, Package, Users as UsersIcon, Calendar, DollarSign, Plus, Trash2, Receipt } from 'lucide-react'; | |
| import { push, set, remove } from 'firebase/database'; | |
| ChartJS.register( | |
| CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, BarElement | |
| ); | |
| export default function Reports() { | |
| const [salesData, setSalesData] = useState([0, 0, 0, 0, 0, 0, 0]); | |
| const [topProducts, setTopProducts] = useState({ labels: [], data: [] }); | |
| const [stats, setStats] = useState({ totalSales: 0, orderCount: 0, avgTicket: 0 }); | |
| const [todaySummary, setTodaySummary] = useState({ Efectivo: 0, Tarjeta: 0, QR: 0 }); | |
| const [expenses, setExpenses] = useState([]); | |
| const [newExpense, setNewExpense] = useState({ desc: '', amount: '', category: 'Insumos' }); | |
| useEffect(() => { | |
| onValue(ref(db, 'orders'), (snapshot) => { | |
| const data = snapshot.val(); | |
| if (data) { | |
| const orders = Object.values(data); | |
| const weeklySales = [0, 0, 0, 0, 0, 0, 0]; | |
| const productCounts = {}; | |
| const todayPayments = { Efectivo: 0, Tarjeta: 0, QR: 0 }; | |
| let total = 0; | |
| const todayStr = new Date().toLocaleDateString(); | |
| orders.forEach(order => { | |
| if (order.status === 'completed') { | |
| total += order.total; | |
| const orderDay = new Date(order.timestamp); | |
| const day = orderDay.getDay(); | |
| const index = (day + 6) % 7; | |
| weeklySales[index] += order.total; | |
| if (orderDay.toLocaleDateString() === todayStr) { | |
| todayPayments[order.paymentMethod || 'Efectivo'] += order.total; | |
| } | |
| order.items.forEach(item => { | |
| productCounts[item.name] = (productCounts[item.name] || 0) + item.qty; | |
| }); | |
| } | |
| }); | |
| setSalesData(weeklySales); | |
| setTodaySummary(todayPayments); | |
| // ... (rest of the stats logic continues) | |
| setStats({ | |
| totalSales: total, | |
| orderCount: orders.filter(o => o.status === 'completed').length, | |
| avgTicket: total / (orders.filter(o => o.status === 'completed').length || 1) | |
| }); | |
| const sortedProducts = Object.entries(productCounts) | |
| .sort((a,b) => b[1] - a[1]) | |
| .slice(0, 5); | |
| setTopProducts({ | |
| labels: sortedProducts.map(p => p[0]), | |
| data: sortedProducts.map(p => p[1]) | |
| }); | |
| } | |
| }); | |
| onValue(ref(db, 'expenses'), (snapshot) => { | |
| const data = snapshot.val(); | |
| setExpenses(data ? Object.keys(data).map(k => ({ id: k, ...data[k] })) : []); | |
| }); | |
| }, []); | |
| const handleAddExpense = async (e) => { | |
| e.preventDefault(); | |
| if (!newExpense.desc || !newExpense.amount) return; | |
| const expRef = push(ref(db, 'expenses')); | |
| await set(expRef, { ...newExpense, amount: parseFloat(newExpense.amount), timestamp: Date.now() }); | |
| setNewExpense({ desc: '', amount: '', category: 'Insumos' }); | |
| }; | |
| const lineData = { | |
| labels: ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'], | |
| datasets: [{ | |
| label: 'Ventas ($)', | |
| data: salesData, | |
| borderColor: '#FF5A5F', | |
| backgroundColor: 'rgba(255, 90, 95, 0.2)', | |
| fill: true, | |
| tension: 0.4 | |
| }] | |
| }; | |
| const barData = { | |
| labels: topProducts.labels, | |
| datasets: [{ | |
| label: 'Unidades Vendidas', | |
| data: topProducts.data, | |
| backgroundColor: 'rgba(0, 166, 153, 0.7)', | |
| borderRadius: 8 | |
| }] | |
| }; | |
| const options = { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { legend: { labels: { color: '#9595a8' } } }, | |
| scales: { | |
| x: { ticks: { color: '#9595a8' }, grid: { color: 'rgba(255,255,255,0.05)' } }, | |
| y: { ticks: { color: '#9595a8' }, grid: { color: 'rgba(255,255,255,0.05)' } } | |
| } | |
| }; | |
| return ( | |
| <div className="animate-fade-in"> | |
| <header style={{ marginBottom: '2.5rem' }}> | |
| <h2 className="text-gradient" style={{ fontSize: '2rem', fontWeight: '800' }}>Análisis de Negocio</h2> | |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.5rem', marginTop: '1.5rem' }}> | |
| <div className="glass-card" style={{ padding: '1.25rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--text-muted)', fontSize: '0.8rem' }}>Ventas Totales <TrendingUp size={16} /></div> | |
| <div style={{ fontSize: '1.5rem', fontWeight: '800', marginTop: '0.5rem' }}>${stats.totalSales.toFixed(2)}</div> | |
| </div> | |
| <div className="glass-card" style={{ padding: '1.25rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--text-muted)', fontSize: '0.8rem' }}>Pedidos <Package size={16} /></div> | |
| <div style={{ fontSize: '1.5rem', fontWeight: '800', marginTop: '0.5rem' }}>{stats.orderCount}</div> | |
| </div> | |
| <div className="glass-card" style={{ padding: '1.25rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--text-muted)', fontSize: '0.8rem' }}>Ticket Promedio <Calendar size={16} /></div> | |
| <div style={{ fontSize: '1.5rem', fontWeight: '800', marginTop: '0.5rem' }}>${stats.avgTicket.toFixed(2)}</div> | |
| </div> | |
| </div> | |
| </header> | |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '2rem' }}> | |
| <div className="glass-card" style={{ height: '350px', display: 'flex', flexDirection: 'column' }}> | |
| <h3 style={{ marginBottom: '1.5rem', fontSize: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| Ventas por Día de la Semana | |
| </h3> | |
| <div style={{ flex: 1, position: 'relative' }}> | |
| <Line data={lineData} options={options} /> | |
| </div> | |
| </div> | |
| <div className="glass-card" style={{ height: '350px', display: 'flex', flexDirection: 'column' }}> | |
| <h3 style={{ marginBottom: '1.5rem', fontSize: '1rem' }}>Productos Estrella</h3> | |
| <div style={{ flex: 1, position: 'relative' }}> | |
| <Bar data={barData} options={options} /> | |
| </div> | |
| </div> | |
| </div> | |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '2rem', marginTop: '2rem' }}> | |
| {/* Arqueo de Caja */} | |
| <div className="glass-card"> | |
| <h3 style={{ marginBottom: '1.5rem', fontSize: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <Receipt size={18} /> Arqueo de Caja (Hoy) | |
| </h3> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> | |
| {Object.entries(todaySummary).map(([method, amount]) => ( | |
| <div key={method} style={{ display: 'flex', justifyContent: 'space-between', padding: '1rem', background: 'rgba(255,255,255,0.02)', borderRadius: '8px' }}> | |
| <span style={{ color: 'var(--text-muted)' }}>{method}</span> | |
| <span style={{ fontWeight: '700' }}>${amount.toFixed(2)}</span> | |
| </div> | |
| ))} | |
| <div style={{ padding: '1rem', marginTop: '0.5rem', background: 'rgba(76,217,100,0.1)', borderRadius: '8px', display: 'flex', justifyContent: 'space-between', border: '1px solid var(--success)' }}> | |
| <span style={{ fontWeight: '700' }}>Corte Total Bruto:</span> | |
| <span style={{ fontWeight: '900', color: 'var(--success)' }}>${Object.values(todaySummary).reduce((a,b) => a + b, 0).toFixed(2)}</span> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Registro de Gastos */} | |
| <div className="glass-card"> | |
| <h3 style={{ marginBottom: '1.5rem', fontSize: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <DollarSign size={18} /> Registro de Egresos | |
| </h3> | |
| <form onSubmit={handleAddExpense} style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem' }}> | |
| <input type="text" placeholder="Descripción" value={newExpense.desc} onChange={e => setNewExpense({...newExpense, desc: e.target.value})} style={{...inputStyle, flex: 2}} /> | |
| <input type="number" placeholder="$" value={newExpense.amount} onChange={e => setNewExpense({...newExpense, amount: e.target.value})} style={{...inputStyle, flex: 1}} /> | |
| <button type="submit" className="btn-primary" style={{ padding: '0 1rem' }}><Plus size={20}/></button> | |
| </form> | |
| <div style={{ maxHeight: '200px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}> | |
| {expenses.length === 0 ? <p style={{ color: 'var(--text-muted)', fontSize: '0.8rem', textAlign: 'center' }}>Sin gastos registrados</p> : | |
| expenses.map(exp => ( | |
| <div key={exp.id} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.85rem', padding: '0.5rem', borderBottom: '1px solid rgba(255,255,255,0.03)' }}> | |
| <span>{exp.desc}</span> | |
| <span style={{ color: 'var(--primary)', fontWeight: '600' }}>-${exp.amount.toFixed(2)}</span> | |
| </div> | |
| )) | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const inputStyle = { width: '100%', padding: '0.7rem', borderRadius: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--border-subtle)', color: '#fff', outline: 'none', fontSize: '0.9rem' }; | |