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.
237 lines
9.5 KiB
TypeScript
237 lines
9.5 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box, Typography, TextField, Button, CircularProgress, Alert,
|
|
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: {
|
|
idDistribuidor: number;
|
|
idEmpresa: number;
|
|
fechaDesde: string;
|
|
fechaHasta: string;
|
|
}) => Promise<void>;
|
|
onCancel: () => void;
|
|
isLoading?: boolean;
|
|
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,
|
|
apiErrorMessage
|
|
}) => {
|
|
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
|
|
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
|
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
|
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
|
|
|
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
|
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
|
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(),
|
|
empresaService.getEmpresasDropdown()
|
|
]);
|
|
setDistribuidores(distData.map(d => d));
|
|
setEmpresas(empData);
|
|
} catch (error) {
|
|
console.error("Error al cargar datos:", error);
|
|
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar distribuidores o empresas.' }));
|
|
} finally {
|
|
setLoadingDropdowns(false);
|
|
}
|
|
};
|
|
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.';
|
|
if (!idEmpresa) errors.idEmpresa = 'Debe seleccionar una empresa.';
|
|
if (!fechaDesde) errors.fechaDesde = 'Fecha Desde es obligatoria.';
|
|
if (!fechaHasta) errors.fechaHasta = 'Fecha Hasta es obligatoria.';
|
|
if (fechaDesde && fechaHasta && new Date(fechaDesde) > new Date(fechaHasta)) {
|
|
errors.fechaHasta = 'Fecha Hasta no puede ser anterior a Fecha Desde.';
|
|
}
|
|
setLocalErrors(errors);
|
|
return Object.keys(errors).length === 0;
|
|
};
|
|
|
|
const handleGenerar = () => {
|
|
if (!validate()) return;
|
|
onGenerarReporte({
|
|
idDistribuidor: Number(idDistribuidor),
|
|
idEmpresa: Number(idEmpresa),
|
|
fechaDesde,
|
|
fechaHasta
|
|
});
|
|
};
|
|
|
|
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>
|
|
Parámetros: Cuenta Corriente Distribuidor
|
|
</Typography>
|
|
<FormControl fullWidth margin="normal" error={!!localErrors.idDistribuidor} disabled={isLoading || loadingDropdowns}>
|
|
<InputLabel id="distribuidor-select-label" required>Distribuidor</InputLabel>
|
|
<Select
|
|
labelId="distribuidor-select-label"
|
|
label="Distribuidor"
|
|
value={idDistribuidor}
|
|
onChange={(e) => { setIdDistribuidor(e.target.value as number); setLocalErrors(p => ({ ...p, idDistribuidor: null })); }}
|
|
>
|
|
<MenuItem value="" disabled><em>Seleccione un distribuidor</em></MenuItem>
|
|
{distribuidores.map((d) => (
|
|
<MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>
|
|
))}
|
|
</Select>
|
|
{localErrors.idDistribuidor && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idDistribuidor}</Typography>}
|
|
</FormControl>
|
|
|
|
<FormControl fullWidth margin="normal" error={!!localErrors.idEmpresa} disabled={isLoading || loadingDropdowns}>
|
|
<InputLabel id="empresa-select-label-cta" required>Empresa</InputLabel>
|
|
<Select
|
|
labelId="empresa-select-label-cta"
|
|
label="Empresa"
|
|
value={idEmpresa}
|
|
onChange={(e) => { setIdEmpresa(e.target.value as number); setLocalErrors(p => ({ ...p, idEmpresa: null })); }}
|
|
>
|
|
<MenuItem value="" disabled><em>Seleccione una empresa</em></MenuItem>
|
|
{empresas.map((e) => (
|
|
<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>
|
|
))}
|
|
</Select>
|
|
{localErrors.idEmpresa && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idEmpresa}</Typography>}
|
|
</FormControl>
|
|
|
|
<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"
|
|
value={fechaHasta}
|
|
onChange={(e) => { setFechaHasta(e.target.value); setLocalErrors(p => ({ ...p, fechaHasta: null })); }}
|
|
margin="normal"
|
|
fullWidth
|
|
required
|
|
error={!!localErrors.fechaHasta}
|
|
helperText={localErrors.fechaHasta}
|
|
disabled={isLoading}
|
|
InputLabelProps={{ shrink: true }}
|
|
/>
|
|
|
|
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
|
|
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
|
|
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
|
<Button onClick={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}>
|
|
{isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'}
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default SeleccionaReporteCuentasDistribuidores;
|