176 lines
6.7 KiB
JavaScript
176 lines
6.7 KiB
JavaScript
import { useState, useEffect } from 'react';
|
||
import { reportsApi } from '../api/client';
|
||
import { formatMoney } from '../utils/calc';
|
||
|
||
const INCLUDE_OPTS = [
|
||
{ key: 'ventas', label: 'Ventas' },
|
||
{ key: 'servicios', label: 'Servicios' },
|
||
{ key: 'alquileres', label: 'Alquileres' },
|
||
{ key: 'gastos_diarios', label: 'Gastos diarios' },
|
||
{ key: 'gastos_fijos', label: 'Gastos fijos' },
|
||
{ key: 'personal', label: 'Personal' },
|
||
];
|
||
|
||
export default function ReportesPage() {
|
||
const [tipo, setTipo] = useState('mensual');
|
||
const [desde, setDesde] = useState('');
|
||
const [hasta, setHasta] = useState('');
|
||
const [include, setInclude] = useState(
|
||
Object.fromEntries(INCLUDE_OPTS.map((o) => [o.key, true]))
|
||
);
|
||
const [report, setReport] = useState(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState(null);
|
||
|
||
const loadReport = async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const data = await reportsApi.get({
|
||
tipo,
|
||
desde: desde || undefined,
|
||
hasta: hasta || undefined,
|
||
include: JSON.stringify(include),
|
||
});
|
||
setReport(data);
|
||
} catch (e) {
|
||
setError(e.message);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
loadReport();
|
||
}, []);
|
||
|
||
const toggleInclude = (key) => setInclude((p) => ({ ...p, [key]: !p[key] }));
|
||
|
||
return (
|
||
<div>
|
||
<h1 className="page-title">Planilla de Reportes</h1>
|
||
|
||
<div className="card" style={{ marginBottom: '1rem' }}>
|
||
<div className="toolbar" style={{ flexWrap: 'wrap' }}>
|
||
<label>
|
||
Tipo de reporte{' '}
|
||
<select value={tipo} onChange={(e) => setTipo(e.target.value)}>
|
||
<option value="diario">Diario</option>
|
||
<option value="semanal">Semanal</option>
|
||
<option value="mensual">Mensual</option>
|
||
<option value="custom">Rango personalizado</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
Desde <input type="date" value={desde} onChange={(e) => setDesde(e.target.value)} />
|
||
</label>
|
||
<label>
|
||
Hasta <input type="date" value={hasta} onChange={(e) => setHasta(e.target.value)} />
|
||
</label>
|
||
<button type="button" className="btn btn-primary" onClick={loadReport} disabled={loading}>
|
||
{loading ? 'Calculando…' : 'Generar reporte'}
|
||
</button>
|
||
</div>
|
||
|
||
<p style={{ fontSize: '0.85rem', color: 'var(--muted)' }}>Planillas a incluir en el cálculo:</p>
|
||
<div className="perm-grid">
|
||
{INCLUDE_OPTS.map((o) => (
|
||
<label key={o.key} className="perm-item checkbox-row">
|
||
<input type="checkbox" checked={!!include[o.key]} onChange={() => toggleInclude(o.key)} />
|
||
{o.label}
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{error && <div className="alert alert-error">{error}</div>}
|
||
|
||
{report && (
|
||
<>
|
||
<div className="card">
|
||
<h2 style={{ marginTop: 0 }}>Desglose por planilla</h2>
|
||
<table className="sheet-table" style={{ minWidth: 'auto' }}>
|
||
<tbody>
|
||
<tr>
|
||
<td>Ventas (ingresos)</td>
|
||
<td className="computed">{formatMoney(report.desglose.ventas.totalIngresos)}</td>
|
||
<td>Utilidad: {formatMoney(report.desglose.ventas.totalUtilidad)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Servicios</td>
|
||
<td className="computed">{formatMoney(report.desglose.servicios.totalIngresos)}</td>
|
||
<td>Utilidad: {formatMoney(report.desglose.servicios.totalUtilidad)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Alquileres</td>
|
||
<td className="computed">{formatMoney(report.desglose.alquileres.totalIngresos)}</td>
|
||
<td>Utilidad: {formatMoney(report.desglose.alquileres.totalUtilidad)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Gastos diarios</td>
|
||
<td colSpan={2} className="computed">{formatMoney(report.desglose.gastosDiarios.total)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Gastos fijos</td>
|
||
<td colSpan={2} className="computed">{formatMoney(report.desglose.gastosFijos.total)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Gastos variables (clasificados)</td>
|
||
<td colSpan={2} className="computed">{formatMoney(report.desglose.gastosVariables.total)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Planilla de personal</td>
|
||
<td colSpan={2} className="computed">{formatMoney(report.desglose.personal.total)}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div className="totals-bar" style={{ marginTop: '1rem' }}>
|
||
<span>Total ingresos: {formatMoney(report.totales.totalIngresos)}</span>
|
||
<span>Total gastos: {formatMoney(report.totales.totalGastos)}</span>
|
||
<span>Total personal: {formatMoney(report.totales.totalPersonal)}</span>
|
||
</div>
|
||
|
||
{report.cierreSemanal && Object.keys(report.cierreSemanal).length > 0 && (
|
||
<div className="card" style={{ marginTop: '1rem' }}>
|
||
<h3>Cierre semanal — gastos diarios por semana</h3>
|
||
<ul>
|
||
{Object.entries(report.cierreSemanal).map(([sem, monto]) => (
|
||
<li key={sem}>
|
||
Semana desde {sem}: <strong>{formatMoney(monto)}</strong>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
<p>
|
||
Total gastos semanales en periodo:{' '}
|
||
<strong>{formatMoney(report.totalGastosSemanales)}</strong>
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{report.cierreMensual && (
|
||
<div className="card" style={{ marginTop: '1rem' }}>
|
||
<h3>Cierre mensual consolidado</h3>
|
||
<p>Gastos fijos en periodo: {formatMoney(report.cierreMensual.gastosFijosEnPeriodo)}</p>
|
||
<p>
|
||
Total gastos mensuales consolidado:{' '}
|
||
<strong>{formatMoney(report.cierreMensual.totalGastosMensualesConsolidado)}</strong>
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className={`report-result ${report.totales.estado}`}>
|
||
Resultado neto ({tipo}): {formatMoney(report.totales.resultadoNeto)} —{' '}
|
||
{report.totales.estado === 'utilidad' ? 'UTILIDAD (Ganancia)' : 'PÉRDIDA'}
|
||
</div>
|
||
|
||
<p style={{ fontSize: '0.8rem', color: 'var(--muted)', marginTop: '0.5rem' }}>
|
||
Fórmula: (Ventas + Alquileres + Servicios) − Total Gastos − Total Personal
|
||
</p>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|