Spaces:
Running
Running
| 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>; | |