first commit

This commit is contained in:
unknown
2026-06-03 16:59:27 -04:00
commit b35cc2f54b
36 changed files with 5634 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
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>
);
}