Files
backendDonMarco/frontend/src/components/GastoSheet.jsx
2026-06-13 12:04:04 -04:00

469 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useMemo, useRef } from 'react';
import { sheetsApi } from '../api/client';
import { useAuth } from '../context/AuthContext';
import { useSheetAutoSave } from '../hooks/useSheetAutoSave';
import { useGestionView } from '../hooks/useGestionView';
import SaveStatus from './SaveStatus';
import GestionBanner from './GestionBanner';
import GestionesSidebar from './GestionesSidebar';
import { emptyGastoRow, formatMoney } from '../utils/calc';
import { isRowEditable } from '../utils/gestion';
export default function GastoSheet({ sheetType, title, defaultClasificacion }) {
const { canEdit, user } = useAuth();
const editable = canEdit(sheetType);
const isAdmin = !!user?.isAdmin;
const { selectedGestionId, selectGestion, sidebarGestiones } = useGestionView();
const [rows, setRows] = useState([emptyGastoRow()]);
const [gestion, setGestion] = useState(null);
const [gestionVista, setGestionVista] = useState(null);
const [esHistorial, setEsHistorial] = useState(false);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState(null);
const skipNotifyRef = useRef(false);
const emptyRow = () => ({ ...emptyGastoRow(), clasificacion: defaultClasificacion });
const canEditSheet = editable && !esHistorial;
const { status, error: saveError, setReady, notifyChange, saveNow } = useSheetAutoSave({
enabled: canEditSheet,
sheetType,
});
useEffect(() => {
let active = true;
setLoading(true);
setLoadError(null);
setReady(false);
(async () => {
try {
const data = await sheetsApi.get(sheetType, { gestionId: selectedGestionId });
if (!active) return;
const loaded = data.rows || [];
skipNotifyRef.current = true;
if (loaded.length) {
setRows(loaded.map((r) => ({ ...r, clasificacion: r.clasificacion || defaultClasificacion })));
} else if (!data.esHistorial && canEditSheet) {
setRows([emptyRow()]);
} else {
setRows([]);
}
setGestion(data.gestion || null);
setGestionVista(data.gestionVista || null);
setEsHistorial(!!data.esHistorial);
setReady(!data.esHistorial);
} catch (e) {
if (active) setLoadError(e.message);
} finally {
if (active) setLoading(false);
}
})();
return () => {
active = false;
};
}, [sheetType, defaultClasificacion, selectedGestionId, setReady, canEditSheet]);
useEffect(() => {
if (loading || !canEditSheet) return;
if (skipNotifyRef.current) {
skipNotifyRef.current = false;
return;
}
notifyChange({ rows });
}, [rows, loading, canEditSheet, notifyChange]);
const total = useMemo(
() => rows.reduce((acc, r) => acc + (Number(r.monto) || 0), 0),
[rows]
);
const updateRow = (index, field, value) => {
if (!isRowEditable(rows[index], canEditSheet, isAdmin)) return;
setRows((prev) => prev.map((r, i) => (i === index ? { ...r, [field]: value } : r)));
};
const addRow = () => setRows((prev) => [...prev, emptyRow()]);
const removeRow = (index) => {
if (!isRowEditable(rows[index], canEditSheet, isAdmin)) return;
skipNotifyRef.current = true;
const next = rows.filter((_, i) => i !== index);
const result = next.length > 0 ? next : [emptyRow()];
setRows(result);
saveNow({ rows: result });
};
const clearSheet = () => {
const msg = gestion?.fechaCierreGestion && !isAdmin
? '¿Eliminar solo las filas editables? Las del período cerrado se conservarán.'
: '¿Vaciar toda la planilla? Se eliminarán todas las filas de la gestión actual.';
if (!window.confirm(msg)) return;
skipNotifyRef.current = true;
if (isAdmin || !gestion?.fechaCierreGestion) {
const blank = [emptyRow()];
setRows(blank);
saveNow({ rows: blank });
return;
}
const kept = rows.filter((r) => r.bloqueada);
const result = kept.length ? kept : [emptyRow()];
setRows(result);
saveNow({ rows: result });
};
if (loading) return <p className="loading">Cargando planilla</p>;
return (
<div className="sheet-layout">
<GestionesSidebar
gestiones={sidebarGestiones}
selectedId={selectedGestionId}
onSelect={selectGestion}
/>
<div className="sheet-layout__main">
<h1 className="page-title">{title}</h1>
{loadError && <div className="alert alert-error">{loadError}</div>}
{esHistorial && gestionVista && (
<div className="historial-badge">
Historial: <strong>{gestionVista.etiqueta}</strong> ({gestionVista.fechaInicio} {' '}
{gestionVista.fechaFin}) solo lectura
</div>
)}
{!esHistorial && <GestionBanner gestion={gestion} isAdmin={isAdmin} />}
{canEditSheet && <SaveStatus status={status} error={saveError} />}
{esHistorial && <div className="alert alert-info">Gestión cerrada solo consulta</div>}
{!editable && !esHistorial && <div className="alert alert-info">Solo lectura</div>}
<div className="toolbar">
{canEditSheet && (
<>
<button type="button" className="btn btn-primary btn-icon" onClick={addRow}>
+
</button>
<button type="button" className="btn btn-secondary" onClick={() => saveNow({ rows })} disabled={status === 'saving'}>
Guardar ahora
</button>
<button type="button" className="btn btn-danger" onClick={clearSheet}>
Vaciar planilla
</button>
</>
)}
</div>
{rows.length === 0 ? (
<p className="loading">Sin registros en esta gestión.</p>
) : (
<div className="card sheet-table-wrap">
<table className="sheet-table">
<thead>
<tr>
<th>Fecha</th>
<th>Detalle</th>
<th>Impuestos</th>
<th> Factura</th>
<th>Monto</th>
<th>Clasificación</th>
{canEditSheet && <th></th>}
</tr>
</thead>
<tbody>
{rows.map((row, i) => {
const rowEditable = isRowEditable(row, canEditSheet, isAdmin);
return (
<tr key={i} className={row.bloqueada ? 'row-locked' : ''}>
<td>
<input type="date" value={row.fecha || ''} disabled={!rowEditable} onChange={(e) => updateRow(i, 'fecha', e.target.value)} />
</td>
<td>
<input value={row.detalle || ''} disabled={!rowEditable} onChange={(e) => updateRow(i, 'detalle', e.target.value)} />
</td>
<td>
<input type="number" min="0" step="any" value={row.impuestos ?? ''} disabled={!rowEditable} onChange={(e) => updateRow(i, 'impuestos', e.target.value)} />
</td>
<td>
<input value={row.numeroFactura || ''} disabled={!rowEditable} onChange={(e) => updateRow(i, 'numeroFactura', e.target.value)} />
</td>
<td>
<input type="number" min="0" step="any" value={row.monto ?? ''} disabled={!rowEditable} onChange={(e) => updateRow(i, 'monto', e.target.value)} />
</td>
<td>
<select value={row.clasificacion || 'diario'} disabled={!rowEditable} onChange={(e) => updateRow(i, 'clasificacion', e.target.value)}>
<option value="diario">Gasto Diario</option>
<option value="fijo">Gasto Fijo</option>
<option value="variable">Gasto Variable</option>
</select>
</td>
{canEditSheet && (
<td>
<button
type="button"
className="btn btn-danger btn-icon"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
removeRow(i);
}}
disabled={!rowEditable}
title={row.bloqueada ? 'Bloqueado — período cerrado' : 'Eliminar fila'}
>
×
</button>
</td>
)}
</tr>
);
})}
</tbody>
</table>
</div>
)}
<div className="totals-bar">
<span>
Total gastos: <strong>{formatMoney(total)}</strong>
</span>
</div>
</div>
</div>
);
}