Spaces:
Running
Running
File size: 9,958 Bytes
564baf9 18f4249 564baf9 18f4249 564baf9 18f4249 564baf9 18f4249 564baf9 18f4249 564baf9 18f4249 564baf9 18f4249 564baf9 76823f0 18f4249 564baf9 18f4249 564baf9 76823f0 564baf9 76823f0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | 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' };
|