restaurante / src /components /admin /FinanceManager.jsx
dimensionalpulsar's picture
Upload 47 files
564baf9 verified
import React, { useState, useEffect } from 'react';
import { db } from '../../firebase/config';
import { ref, onValue, push, set } from 'firebase/database';
import { DollarSign, ArrowUpCircle, ArrowDownCircle, Save, Calendar, Clock, History } from 'lucide-react';
export default function FinanceManager() {
const [cashSessions, setCashSessions] = useState([]);
const [expenses, setExpenses] = useState([]);
const [activeSession, setActiveSession] = useState(null);
const [openingBalance, setOpeningBalance] = useState('');
const [expenseData, setExpenseData] = useState({ concept: '', amount: '', category: 'Varios' });
useEffect(() => {
// Fetch sessions
onValue(ref(db, 'finance/sessions'), (snapshot) => {
const data = snapshot.val();
if (data) {
const list = Object.keys(data).map(id => ({ id, ...data[id] })).sort((a,b) => b.openedAt - a.openedAt);
setCashSessions(list);
const active = list.find(s => s.status === 'open');
setActiveSession(active || null);
}
});
// Fetch expenses
onValue(ref(db, 'finance/expenses'), (snapshot) => {
const data = snapshot.val();
setExpenses(data ? Object.keys(data).map(id => ({ id, ...data[id] })).sort((a,b) => b.timestamp - a.timestamp) : []);
});
}, []);
const openCash = async () => {
if (!openingBalance) return;
const newSessionRef = push(ref(db, 'finance/sessions'));
await set(newSessionRef, {
openedAt: Date.now(),
openingBalance: parseFloat(openingBalance),
status: 'open',
totalSales: 0,
totalExpenses: 0
});
setOpeningBalance('');
};
const closeCash = async () => {
if (!activeSession) return;
if (window.confirm('¿Deseas cerrar el turno de caja actual?')) {
await set(ref(db, `finance/sessions/${activeSession.id}/status`), 'closed');
await set(ref(db, `finance/sessions/${activeSession.id}/closedAt`), Date.now());
}
};
const addExpense = async (e) => {
e.preventDefault();
if (!expenseData.concept || !expenseData.amount) return;
const expenseRef = push(ref(db, 'finance/expenses'));
await set(expenseRef, {
...expenseData,
amount: parseFloat(expenseData.amount),
timestamp: Date.now(),
sessionId: activeSession?.id || 'none'
});
setExpenseData({ concept: '', amount: '', category: 'Varios' });
};
return (
<div className="animate-fade-in" style={{ padding: '0 1rem' }}>
<header style={{ marginBottom: '2.5rem' }}>
<h2 className="text-gradient" style={{ fontSize: '2rem', fontWeight: '800', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<DollarSign size={28} /> Control Financiero
</h2>
<p style={{ color: 'var(--text-muted)' }}>Arqueo de caja, egresos y flujo de efectivo semanal</p>
</header>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(350px, 1fr))', gap: '2rem' }}>
{/* Cash Arqueo Module */}
<div className="glass-card" style={{ border: activeSession ? '1px solid var(--success)' : '1px solid var(--border-subtle)' }}>
<h3 style={{ marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Clock size={20} /> Turno de Caja (Arqueo)
</h3>
{!activeSession ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<p style={{ fontSize: '0.9rem', color: 'var(--text-muted)' }}>Caja cerrada actualmente. Ingresa el fondo de apertura para iniciar.</p>
<input
type="number"
placeholder="Monto de Apertura ($)"
value={openingBalance}
onChange={(e) => setOpeningBalance(e.target.value)}
style={inputStyle}
/>
<button onClick={openCash} className="btn-primary" style={{ padding: '0.8rem' }}>
Abrir Turno
</button>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<div style={statBoxStyle}>
<span style={{ fontSize: '0.75rem', opacity: 0.6 }}>Apertura</span>
<div style={{ fontSize: '1.2rem', fontWeight: '800' }}>${activeSession.openingBalance.toFixed(2)}</div>
</div>
<div style={statBoxStyle}>
<span style={{ fontSize: '0.75rem', opacity: 0.6 }}>Hora</span>
<div style={{ fontSize: '1rem', fontWeight: '600' }}>{new Date(activeSession.openedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</div>
</div>
</div>
<div style={{ ...statBoxStyle, background: 'rgba(76,217,100,0.05)', borderColor: 'var(--success)' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--success)' }}>Ventas Registradas</span>
<div style={{ fontSize: '1.5rem', fontWeight: '900', color: 'var(--success)' }}>$0.00</div>
<p style={{ fontSize: '0.7rem', opacity: 0.5, marginTop: '0.2rem' }}>Sincronizado con POS en tiempo real</p>
</div>
<button onClick={closeCash} className="btn-glass" style={{ color: 'var(--primary)', borderColor: 'var(--primary)', padding: '0.8rem' }}>
Cerrar Turno / Corte
</button>
</div>
)}
</div>
{/* Expenses Module */}
<div className="glass-card">
<h3 style={{ marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<ArrowDownCircle size={20} /> Registro de Egresos
</h3>
<form onSubmit={addExpense} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<input type="text" placeholder="Concepto (ej. Pago luz, Propina extra)" value={expenseData.concept} onChange={e => setExpenseData({...expenseData, concept: e.target.value})} style={inputStyle} />
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<input type="number" placeholder="Monto ($)" value={expenseData.amount} onChange={e => setExpenseData({...expenseData, amount: e.target.value})} style={inputStyle} />
<select value={expenseData.category} onChange={e => setExpenseData({...expenseData, category: e.target.value})} style={inputStyle}>
<option value="Varios">Varios</option>
<option value="Compras">Compras</option>
<option value="Servicios">Servicios</option>
<option value="Sueldos">Sueldos</option>
</select>
</div>
<button type="submit" className="btn-glass" style={{ padding: '0.8rem' }}>
<Plus size={18} /> Registrar Egreso
</button>
</form>
<div style={{ marginTop: '2rem', maxHeight: '200px', overflowY: 'auto' }}>
<h4 style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '1rem', borderBottom: '1px solid var(--border-subtle)', paddingBottom: '0.5rem' }}>Últimos Gastos</h4>
{expenses.map(exp => (
<div key={exp.id} style={{ display: 'flex', justifyContent: 'space-between', padding: '0.5rem 0', fontSize: '0.9rem' }}>
<span>{exp.concept}</span>
<span style={{ color: 'var(--primary)', fontWeight: '700' }}>-${exp.amount.toFixed(2)}</span>
</div>
))}
{expenses.length === 0 && <p style={{ fontSize: '0.85rem', color: 'var(--text-muted)', textAlign: 'center' }}>No hay gastos registrados</p>}
</div>
</div>
</div>
<div className="glass-panel" style={{ marginTop: '3rem', padding: '1.5rem' }}>
<h3 style={{ fontSize: '1.1rem', marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<History size={18} /> Historial de Turnos Recientes
</h3>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.9rem' }}>
<thead>
<tr style={{ textAlign: 'left', borderBottom: '1px solid var(--border-subtle)', opacity: 0.6 }}>
<th style={{ padding: '1rem' }}>Fecha</th>
<th style={{ padding: '1rem' }}>Apertura</th>
<th style={{ padding: '1rem' }}>Cierre</th>
<th style={{ padding: '1rem' }}>Ventas</th>
<th style={{ padding: '1rem' }}>Estado</th>
</tr>
</thead>
<tbody>
{cashSessions.map(sess => (
<tr key={sess.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.03)' }}>
<td style={{ padding: '1rem' }}>{new Date(sess.openedAt).toLocaleDateString()}</td>
<td style={{ padding: '1rem' }}>${sess.openingBalance.toFixed(2)}</td>
<td style={{ padding: '1rem' }}>{sess.closedAt ? new Date(sess.closedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '-'}</td>
<td style={{ padding: '1rem' }}>${(sess.totalSales || 0).toFixed(2)}</td>
<td style={{ padding: '1rem' }}>
<span style={{ padding: '2px 8px', borderRadius: '4px', background: sess.status === 'open' ? 'rgba(76,217,100,0.1)' : 'rgba(255,255,255,0.05)', color: sess.status === 'open' ? 'var(--success)' : 'var(--text-muted)', fontSize: '0.75rem' }}>
{sess.status.toUpperCase()}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
const inputStyle = { width: '100%', padding: '0.8rem', borderRadius: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--border-subtle)', color: '#fff', outline: 'none' };
const statBoxStyle = { padding: '1rem', borderRadius: '12px', background: 'rgba(255,255,255,0.02)', border: '1px solid var(--border-subtle)', display: 'flex', flexDirection: 'column', gap: '0.25rem' };
const Plus = ({ size }) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>;