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.
236 lines
9.4 KiB
TypeScript
236 lines
9.4 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
|
FormControl, InputLabel, Select, MenuItem
|
|
} from '@mui/material';
|
|
import type { CrearCierreDto } from '../../../models/dtos/Contables/CrearCierreDto';
|
|
import type { CierreCuentaCorrienteDto } from '../../../models/dtos/Contables/CierreCuentaCorrienteDto';
|
|
import type { DistribuidorDropdownDto } from '../../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
|
import type { EmpresaDropdownDto } from '../../../models/dtos/Distribucion/EmpresaDropdownDto';
|
|
import distribuidorService from '../../../services/Distribucion/distribuidorService';
|
|
import empresaService from '../../../services/Distribucion/empresaService';
|
|
|
|
const modalStyle = {
|
|
position: 'absolute' as const,
|
|
top: '50%',
|
|
left: '50%',
|
|
transform: 'translate(-50%, -50%)',
|
|
width: { xs: '90%', sm: 600 },
|
|
bgcolor: 'background.paper',
|
|
border: '2px solid #000',
|
|
boxShadow: 24,
|
|
p: 4,
|
|
maxHeight: '90vh',
|
|
overflowY: 'auto'
|
|
};
|
|
|
|
interface NuevoCierreModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (data: CrearCierreDto) => Promise<CierreCuentaCorrienteDto>;
|
|
initialIdDistribuidor?: number | null;
|
|
initialIdEmpresa?: number | null;
|
|
errorMessage?: string | null;
|
|
clearErrorMessage: () => void;
|
|
}
|
|
|
|
const todayIso = () => new Date().toISOString().split('T')[0];
|
|
|
|
const NuevoCierreModal: React.FC<NuevoCierreModalProps> = ({
|
|
open,
|
|
onClose,
|
|
onSubmit,
|
|
initialIdDistribuidor,
|
|
initialIdEmpresa,
|
|
errorMessage,
|
|
clearErrorMessage
|
|
}) => {
|
|
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
|
|
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
|
const [fechaCorte, setFechaCorte] = useState<string>(todayIso());
|
|
const [justificacion, setJustificacion] = useState<string>('');
|
|
|
|
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
|
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
|
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
|
|
|
useEffect(() => {
|
|
const fetchDropdownData = async () => {
|
|
setLoadingDropdowns(true);
|
|
try {
|
|
const [distData, empData] = await Promise.all([
|
|
distribuidorService.getAllDistribuidoresDropdown(),
|
|
empresaService.getEmpresasDropdown()
|
|
]);
|
|
setDistribuidores(distData);
|
|
setEmpresas(empData);
|
|
} catch (err) {
|
|
console.error('Error al cargar dropdowns', err);
|
|
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar distribuidores o empresas.' }));
|
|
} finally {
|
|
setLoadingDropdowns(false);
|
|
}
|
|
};
|
|
|
|
if (open) {
|
|
fetchDropdownData();
|
|
setIdDistribuidor(initialIdDistribuidor ?? '');
|
|
setIdEmpresa(initialIdEmpresa ?? '');
|
|
setFechaCorte(todayIso());
|
|
setJustificacion('');
|
|
setLocalErrors({});
|
|
clearErrorMessage();
|
|
}
|
|
}, [open, initialIdDistribuidor, initialIdEmpresa, clearErrorMessage]);
|
|
|
|
const validate = (): boolean => {
|
|
const errors: { [key: string]: string | null } = {};
|
|
if (!idDistribuidor) errors.idDistribuidor = 'Seleccione un distribuidor.';
|
|
if (!idEmpresa) errors.idEmpresa = 'Seleccione una empresa.';
|
|
if (!fechaCorte) {
|
|
errors.fechaCorte = 'La fecha de corte es obligatoria.';
|
|
} else if (new Date(fechaCorte) > new Date(todayIso())) {
|
|
errors.fechaCorte = 'La fecha de corte no puede ser futura.';
|
|
}
|
|
if (justificacion.length > 500) {
|
|
errors.justificacion = 'La justificación no puede superar los 500 caracteres.';
|
|
}
|
|
|
|
setLocalErrors(errors);
|
|
return Object.keys(errors).length === 0;
|
|
};
|
|
|
|
const handleInputChange = (fieldName: string) => {
|
|
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
|
if (errorMessage) clearErrorMessage();
|
|
};
|
|
|
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
clearErrorMessage();
|
|
if (!validate()) return;
|
|
|
|
const distSeleccionado = distribuidores.find(d => d.idDistribuidor === Number(idDistribuidor));
|
|
const empSeleccionada = empresas.find(e => e.idEmpresa === Number(idEmpresa));
|
|
const confirmMsg = `Estás por cerrar el período hasta ${fechaCorte} para "${distSeleccionado?.nombre ?? ''}" en "${empSeleccionada?.nombre ?? ''}".\n\n` +
|
|
`Después de cerrar no se podrán registrar movimientos, pagos, notas ni ajustes con fecha menor o igual a esta. ¿Continuar?`;
|
|
if (!window.confirm(confirmMsg)) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const dto: CrearCierreDto = {
|
|
idDistribuidor: Number(idDistribuidor),
|
|
idEmpresa: Number(idEmpresa),
|
|
fechaCorte,
|
|
justificacion: justificacion.trim() || null
|
|
};
|
|
await onSubmit(dto);
|
|
onClose();
|
|
} catch (err) {
|
|
console.error('Error al crear cierre:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal open={open} onClose={onClose}>
|
|
<Box sx={modalStyle}>
|
|
<Typography variant="h6" component="h2" gutterBottom>
|
|
Nuevo Cierre de Cuenta Corriente
|
|
</Typography>
|
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
|
<FormControl fullWidth margin="dense" error={!!localErrors.idDistribuidor} required>
|
|
<InputLabel id="distribuidor-cierre-label">Distribuidor</InputLabel>
|
|
<Select
|
|
labelId="distribuidor-cierre-label"
|
|
label="Distribuidor"
|
|
value={idDistribuidor}
|
|
onChange={(e) => { setIdDistribuidor(e.target.value as number); handleInputChange('idDistribuidor'); }}
|
|
disabled={loading || loadingDropdowns}
|
|
>
|
|
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
|
{distribuidores.map((d) => (
|
|
<MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>
|
|
))}
|
|
</Select>
|
|
{localErrors.idDistribuidor && <Typography color="error" variant="caption">{localErrors.idDistribuidor}</Typography>}
|
|
</FormControl>
|
|
|
|
<FormControl fullWidth margin="dense" error={!!localErrors.idEmpresa} required>
|
|
<InputLabel id="empresa-cierre-label">Empresa</InputLabel>
|
|
<Select
|
|
labelId="empresa-cierre-label"
|
|
label="Empresa"
|
|
value={idEmpresa}
|
|
onChange={(e) => { setIdEmpresa(e.target.value as number); handleInputChange('idEmpresa'); }}
|
|
disabled={loading || loadingDropdowns}
|
|
>
|
|
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
|
{empresas.map((e) => (
|
|
<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>
|
|
))}
|
|
</Select>
|
|
{localErrors.idEmpresa && <Typography color="error" variant="caption">{localErrors.idEmpresa}</Typography>}
|
|
</FormControl>
|
|
|
|
<TextField
|
|
label="Fecha de Corte"
|
|
type="date"
|
|
value={fechaCorte}
|
|
required
|
|
onChange={(e) => { setFechaCorte(e.target.value); handleInputChange('fechaCorte'); }}
|
|
margin="dense"
|
|
fullWidth
|
|
error={!!localErrors.fechaCorte}
|
|
helperText={localErrors.fechaCorte || 'No puede ser una fecha futura.'}
|
|
disabled={loading}
|
|
InputLabelProps={{ shrink: true }}
|
|
inputProps={{ max: todayIso() }}
|
|
/>
|
|
|
|
<Alert severity="info" sx={{ mt: 0.5 }}>
|
|
La fecha de corte es inclusive: los movimientos con fecha igual a la seleccionada se
|
|
incluyen en el cálculo del saldo de cierre.
|
|
</Alert>
|
|
|
|
<TextField
|
|
label="Justificación (opcional)"
|
|
value={justificacion}
|
|
onChange={(e) => { setJustificacion(e.target.value); handleInputChange('justificacion'); }}
|
|
margin="dense"
|
|
fullWidth
|
|
multiline
|
|
rows={3}
|
|
disabled={loading}
|
|
error={!!localErrors.justificacion}
|
|
helperText={localErrors.justificacion || `${justificacion.length}/500 caracteres`}
|
|
inputProps={{ maxLength: 500 }}
|
|
/>
|
|
</Box>
|
|
|
|
<Alert severity="warning" sx={{ mt: 2 }}>
|
|
Una vez creado el cierre, no podrán registrarse, modificarse ni eliminarse pagos, notas, ajustes
|
|
o movimientos cuya fecha de operación sea menor o igual a la fecha de corte.
|
|
</Alert>
|
|
|
|
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</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={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
|
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
|
|
{loading ? <CircularProgress size={24} /> : 'Crear Cierre'}
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default NuevoCierreModal;
|