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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user