feat(contables): cierre mensual de cuenta corriente de distribuidor

Permite congelar el saldo de un distribuidor por empresa a una fecha de
corte y bloquear modificaciones retroactivas sobre el período cerrado.
El saldo se calcula sumando movimientos en rango (sin tocar cue_Saldos).
Incluye reapertura controlada exclusivamente por SuperAdmin, reporte con
saldo inicial, atajo "Desde último cierre", y auditoría del ciclo de
vida _H. Permisos CC001/CC002/CC003. Middleware global mapea bloqueos
por período cerrado a HTTP 409.
This commit is contained in:
2026-05-07 12:03:26 -03:00
parent 7e274ef114
commit 24eaf18fd9
62 changed files with 2813 additions and 162 deletions

View File

@@ -1,12 +1,14 @@
import React, { useState, useEffect } from 'react';
import {
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem
FormControl, InputLabel, Select, MenuItem, Tooltip
} from '@mui/material';
import EventAvailableIcon from '@mui/icons-material/EventAvailable';
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
import distribuidorService from '../../services/Distribucion/distribuidorService';
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
import empresaService from '../../services/Distribucion/empresaService';
import cierresCcService from '../../services/Contables/cierresCcService';
interface SeleccionaReporteCuentasDistribuidoresProps {
onGenerarReporte: (params: {
@@ -20,6 +22,18 @@ interface SeleccionaReporteCuentasDistribuidoresProps {
apiErrorMessage?: string | null;
}
// Suma 1 día a una fecha en formato yyyy-MM-dd y devuelve la fecha resultante
// también en yyyy-MM-dd. Usado por el atajo "Desde último cierre" para arrancar
// el reporte el día siguiente al cierre.
const addOneDay = (yyyyMmDd: string): string => {
const d = new Date(yyyyMmDd + 'T00:00:00');
d.setDate(d.getDate() + 1);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasDistribuidoresProps> = ({
onGenerarReporte,
isLoading,
@@ -35,15 +49,19 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const [loadingUltimoCierre, setLoadingUltimoCierre] = useState(false);
const [hayCierrePrevio, setHayCierrePrevio] = useState<boolean | null>(null);
const [infoCierre, setInfoCierre] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
setLoadingDropdowns(true);
try {
const [distData, empData] = await Promise.all([
distribuidorService.getAllDistribuidoresDropdown(), // Asume que este servicio existe
empresaService.getEmpresasDropdown() // Asume que este servicio existe
distribuidorService.getAllDistribuidoresDropdown(),
empresaService.getEmpresasDropdown()
]);
setDistribuidores(distData.map(d => d)); // El servicio devuelve tupla
setDistribuidores(distData.map(d => d));
setEmpresas(empData);
} catch (error) {
console.error("Error al cargar datos:", error);
@@ -55,6 +73,13 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
fetchData();
}, []);
// Reset del estado del atajo cuando cambian distribuidor/empresa: el "ultimo cierre"
// depende del par (distribuidor, empresa), si alguno cambia hay que volver a chequear.
useEffect(() => {
setHayCierrePrevio(null);
setInfoCierre(null);
}, [idDistribuidor, idEmpresa]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idDistribuidor) errors.idDistribuidor = 'Debe seleccionar un distribuidor.';
@@ -78,6 +103,37 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
});
};
const handleDesdeUltimoCierre = async () => {
if (!idDistribuidor || !idEmpresa) return;
setLoadingUltimoCierre(true);
setInfoCierre(null);
try {
const ultimo = await cierresCcService.getUltimoCierre(Number(idDistribuidor), Number(idEmpresa));
if (ultimo === null) {
setHayCierrePrevio(false);
setInfoCierre('Sin cierres previos para este distribuidor y empresa.');
} else {
const nuevaFechaDesde = addOneDay(ultimo.fechaCorte);
setFechaDesde(nuevaFechaDesde);
setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null }));
setHayCierrePrevio(true);
setInfoCierre(`Último cierre: ${ultimo.fechaCorte}. Fecha Desde ajustada al día siguiente.`);
}
} catch (err) {
console.error('Error al obtener último cierre:', err);
setInfoCierre('Error al consultar el último cierre.');
} finally {
setLoadingUltimoCierre(false);
}
};
const atajoDisabled = !idDistribuidor || !idEmpresa || loadingUltimoCierre || isLoading || hayCierrePrevio === false;
const atajoTooltip = !idDistribuidor || !idEmpresa
? 'Seleccioná distribuidor y empresa primero.'
: hayCierrePrevio === false
? 'Sin cierres previos.'
: 'Autocompleta Fecha Desde con el día siguiente al último cierre.';
return (
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
<Typography variant="h6" gutterBottom>
@@ -115,19 +171,42 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
{localErrors.idEmpresa && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idEmpresa}</Typography>}
</FormControl>
<TextField
label="Fecha Desde"
type="date"
value={fechaDesde}
onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }}
margin="normal"
fullWidth
required
error={!!localErrors.fechaDesde}
helperText={localErrors.fechaDesde}
disabled={isLoading}
InputLabelProps={{ shrink: true }}
/>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
<TextField
label="Fecha Desde"
type="date"
value={fechaDesde}
onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }}
margin="normal"
fullWidth
required
error={!!localErrors.fechaDesde}
helperText={localErrors.fechaDesde}
disabled={isLoading}
InputLabelProps={{ shrink: true }}
/>
<Tooltip title={atajoTooltip}>
<span>
<Button
variant="outlined"
size="small"
startIcon={loadingUltimoCierre ? <CircularProgress size={16} /> : <EventAvailableIcon />}
onClick={handleDesdeUltimoCierre}
disabled={atajoDisabled}
sx={{ mt: 2, whiteSpace: 'nowrap' }}
>
Desde último cierre
</Button>
</span>
</Tooltip>
</Box>
{infoCierre && (
<Alert severity={hayCierrePrevio ? 'success' : 'info'} sx={{ mt: 1 }}>
{infoCierre}
</Alert>
)}
<TextField
label="Fecha Hasta"
type="date"
@@ -154,4 +233,4 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
);
};
export default SeleccionaReporteCuentasDistribuidores;
export default SeleccionaReporteCuentasDistribuidores;