From 9201d7222bfb88cc397b50de7b604b8115b8174f Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 11 Feb 2026 14:52:58 -0300 Subject: [PATCH] Fix: Fechas de Estado Bobinas - Las fechas de estado de las bobinas no pueden ser anterior a la fecha de remito (ingreso). --- .../Services/Impresion/StockBobinaService.cs | 10 +- .../StockBobinaCambioEstadoModal.tsx | 499 +++++++++--------- 2 files changed, 266 insertions(+), 243 deletions(-) diff --git a/Backend/GestionIntegral.Api/Services/Impresion/StockBobinaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/StockBobinaService.cs index e5aa91e..10aca90 100644 --- a/Backend/GestionIntegral.Api/Services/Impresion/StockBobinaService.cs +++ b/Backend/GestionIntegral.Api/Services/Impresion/StockBobinaService.cs @@ -199,12 +199,20 @@ namespace GestionIntegral.Api.Services.Impresion using var transaction = connection.BeginTransaction(); try { - var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de la transacción + var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); if (bobina == null) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); } + + // Comparamos solo las fechas (sin hora) para evitar problemas de precisión. + if (cambiarEstadoDto.FechaCambioEstado.Date < bobina.FechaRemito.Date) + { + try { transaction.Rollback(); } catch { } + return (false, $"Error de integridad: La fecha del nuevo estado ({cambiarEstadoDto.FechaCambioEstado:dd/MM/yyyy}) " + + $"no puede ser anterior a la fecha de ingreso por remito ({bobina.FechaRemito:dd/MM/yyyy})."); + } var nuevoEstado = await _estadoBobinaRepository.GetByIdAsync(cambiarEstadoDto.NuevoEstadoId); if (nuevoEstado == null) diff --git a/Frontend/src/components/Modals/Impresion/StockBobinaCambioEstadoModal.tsx b/Frontend/src/components/Modals/Impresion/StockBobinaCambioEstadoModal.tsx index feb3c58..b0ea89c 100644 --- a/Frontend/src/components/Modals/Impresion/StockBobinaCambioEstadoModal.tsx +++ b/Frontend/src/components/Modals/Impresion/StockBobinaCambioEstadoModal.tsx @@ -7,7 +7,6 @@ import { import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto'; import type { CambiarEstadoBobinaDto } from '../../../models/dtos/Impresion/CambiarEstadoBobinaDto'; import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto'; -// --- CAMBIO: Importar PublicacionDropdownDto --- import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto'; import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto'; import estadoBobinaService from '../../../services/Impresion/estadoBobinaService'; @@ -33,272 +32,288 @@ const ID_ESTADO_EN_USO = 2; // Usaremos este consistentemente const ID_ESTADO_DANADA = 3; interface StockBobinaCambioEstadoModalProps { - open: boolean; - onClose: () => void; - onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise; - bobinaActual: StockBobinaDto | null; - errorMessage?: string | null; - clearErrorMessage: () => void; + open: boolean; + onClose: () => void; + onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise; + bobinaActual: StockBobinaDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; } const StockBobinaCambioEstadoModal: React.FC = ({ - open, - onClose, - onSubmit, - bobinaActual, - errorMessage, - clearErrorMessage + open, + onClose, + onSubmit, + bobinaActual, + errorMessage, + clearErrorMessage }) => { - const [nuevoEstadoId, setNuevoEstadoId] = useState(''); - const [idPublicacion, setIdPublicacion] = useState(''); - const [idSeccion, setIdSeccion] = useState(''); - const [obs, setObs] = useState(''); - const [fechaCambioEstado, setFechaCambioEstado] = useState(''); + const [nuevoEstadoId, setNuevoEstadoId] = useState(''); + const [idPublicacion, setIdPublicacion] = useState(''); + const [idSeccion, setIdSeccion] = useState(''); + const [obs, setObs] = useState(''); + const [fechaCambioEstado, setFechaCambioEstado] = useState(''); - const [estadosDisponibles, setEstadosDisponibles] = useState([]); - // --- CAMBIO: Usar PublicacionDropdownDto para el estado --- - const [publicacionesDisponibles, setPublicacionesDisponibles] = useState([]); - const [seccionesDisponibles, setSeccionesDisponibles] = useState([]); + const [estadosDisponibles, setEstadosDisponibles] = useState([]); + const [publicacionesDisponibles, setPublicacionesDisponibles] = useState([]); + const [seccionesDisponibles, setSeccionesDisponibles] = useState([]); - const [loading, setLoading] = useState(false); - const [loadingDropdowns, setLoadingDropdowns] = useState(false); - const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); - useEffect(() => { - const fetchDropdownData = async () => { - if (!bobinaActual) return; - setLoadingDropdowns(true); - try { - const todosLosEstados = await estadoBobinaService.getAllEstadosBobina(); - let estadosFiltrados: EstadoBobinaDto[]; - - if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) { - estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DISPONIBLE); - } else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) { - estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA); - } else if (bobinaActual.idEstadoBobina === ID_ESTADO_DISPONIBLE) { - // --- CAMBIO: Usar ID_ESTADO_EN_USO --- - estadosFiltrados = todosLosEstados.filter( - e => e.idEstadoBobina === ID_ESTADO_EN_USO || e.idEstadoBobina === ID_ESTADO_DANADA - ); - } else { - estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina); - } - - setEstadosDisponibles(estadosFiltrados); - - const sePuedePonerEnUso = estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO); - - if (sePuedePonerEnUso) { - // --- CAMBIO: La data es PublicacionDropdownDto[] --- - const publicacionesData: PublicacionDropdownDto[] = await publicacionService.getPublicacionesForDropdown(true); - setPublicacionesDisponibles(publicacionesData); - } else { - setPublicacionesDisponibles([]); - setIdPublicacion(''); - setIdSeccion(''); - } - - } catch (error) { - console.error("Error al cargar datos para dropdowns (Cambio Estado Bobina)", error); - setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'})); - } finally { - setLoadingDropdowns(false); - } - }; - - if (open && bobinaActual) { - fetchDropdownData(); - setNuevoEstadoId(''); - // Pre-cargar basado en si la bobina actual está "En Uso" - if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) { - setIdPublicacion(bobinaActual.idPublicacion?.toString() || ''); - // Solo pre-cargar sección si la publicación también estaba pre-cargada - if (bobinaActual.idPublicacion) { - setIdSeccion(bobinaActual.idSeccion?.toString() || ''); - } else { - setIdSeccion(''); - } - } else { - setIdPublicacion(''); - setIdSeccion(''); - } - setObs(bobinaActual.obs || ''); - setFechaCambioEstado(new Date().toISOString().split('T')[0]); - setLocalErrors({}); - clearErrorMessage(); - } - }, [open, bobinaActual, clearErrorMessage]); - - - useEffect(() => { - const fetchSecciones = async () => { - if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO && idPublicacion) { + useEffect(() => { + const fetchDropdownData = async () => { + if (!bobinaActual) return; setLoadingDropdowns(true); try { - const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true); - setSeccionesDisponibles(data); + const todosLosEstados = await estadoBobinaService.getAllEstadosBobina(); + let estadosFiltrados: EstadoBobinaDto[]; + + if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) { + estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DISPONIBLE); + } else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) { + estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA); + } else if (bobinaActual.idEstadoBobina === ID_ESTADO_DISPONIBLE) { + estadosFiltrados = todosLosEstados.filter( + e => e.idEstadoBobina === ID_ESTADO_EN_USO || e.idEstadoBobina === ID_ESTADO_DANADA + ); + } else { + estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina); + } + + setEstadosDisponibles(estadosFiltrados); + + const sePuedePonerEnUso = estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO); + + if (sePuedePonerEnUso) { + const publicacionesData: PublicacionDropdownDto[] = await publicacionService.getPublicacionesForDropdown(true); + setPublicacionesDisponibles(publicacionesData); + } else { + setPublicacionesDisponibles([]); + setIdPublicacion(''); + setIdSeccion(''); + } + } catch (error) { - console.error("Error al cargar secciones:", error); - setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.'})); - setSeccionesDisponibles([]); + console.error("Error al cargar datos para dropdowns (Cambio Estado Bobina)", error); + setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar datos necesarios.' })); } finally { setLoadingDropdowns(false); } + }; + + if (open && bobinaActual) { + fetchDropdownData(); + setNuevoEstadoId(''); + // Pre-cargar basado en si la bobina actual está "En Uso" + if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) { + setIdPublicacion(bobinaActual.idPublicacion?.toString() || ''); + // Solo pre-cargar sección si la publicación también estaba pre-cargada + if (bobinaActual.idPublicacion) { + setIdSeccion(bobinaActual.idSeccion?.toString() || ''); + } else { + setIdSeccion(''); + } + } else { + setIdPublicacion(''); + setIdSeccion(''); + } + setObs(bobinaActual.obs || ''); + setFechaCambioEstado(new Date().toISOString().split('T')[0]); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, bobinaActual, clearErrorMessage]); + + + useEffect(() => { + const fetchSecciones = async () => { + if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO && idPublicacion) { + setLoadingDropdowns(true); + try { + const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true); + setSeccionesDisponibles(data); + } catch (error) { + console.error("Error al cargar secciones:", error); + setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.' })); + setSeccionesDisponibles([]); + } finally { + setLoadingDropdowns(false); + } + } else { + setSeccionesDisponibles([]); + // No es necesario setIdSeccion('') aquí si el useEffect de nuevoEstadoId ya lo hace. + } + }; + if (idPublicacion && Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { // Solo fetchear si hay idPublicacion + fetchSecciones(); } else { - setSeccionesDisponibles([]); - // No es necesario setIdSeccion('') aquí si el useEffect de nuevoEstadoId ya lo hace. + setSeccionesDisponibles([]); // Limpiar si no se cumplen condiciones + } + }, [nuevoEstadoId, idPublicacion]); + + + // Efecto para limpiar publicacion/seccion si el nuevo estado no es "En Uso" + useEffect(() => { + if (Number(nuevoEstadoId) !== ID_ESTADO_EN_USO) { + setIdPublicacion(''); + setIdSeccion(''); + } + }, [nuevoEstadoId]); + + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.'; + if (!fechaCambioEstado.trim()) { + errors.fechaCambioEstado = 'La fecha es obligatoria.'; + } else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaCambioEstado)) { + errors.fechaCambioEstado = 'Formato de fecha inválido.'; + } else if (bobinaActual) { + const fechaRemitoSimple = bobinaActual.fechaRemito.split('T')[0]; + if (fechaCambioEstado < fechaRemitoSimple) { + errors.fechaCambioEstado = `La fecha no puede ser anterior al ingreso (${fechaRemitoSimple}).`; + } + } + + if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { + if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.'; + if (!idSeccion) errors.idSeccion = 'Seleccione una sección.'; + } + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + if (errorMessage) clearErrorMessage(); + // La lógica de limpieza de pub/secc se movió a un useEffect dedicado a nuevoEstadoId + // y el de sección a un useEffect de idPublicacion + if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion + setIdSeccion(''); } }; - if (idPublicacion && Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { // Solo fetchear si hay idPublicacion - fetchSecciones(); - } else { - setSeccionesDisponibles([]); // Limpiar si no se cumplen condiciones - } - }, [nuevoEstadoId, idPublicacion]); + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate() || !bobinaActual) return; - // Efecto para limpiar publicacion/seccion si el nuevo estado no es "En Uso" - useEffect(() => { - if (Number(nuevoEstadoId) !== ID_ESTADO_EN_USO) { - setIdPublicacion(''); - setIdSeccion(''); - } - }, [nuevoEstadoId]); + setLoading(true); + try { + const esEnUso = Number(nuevoEstadoId) === ID_ESTADO_EN_USO; + const dataToSubmit: CambiarEstadoBobinaDto = { + nuevoEstadoId: Number(nuevoEstadoId), + idPublicacion: esEnUso && idPublicacion ? Number(idPublicacion) : null, + idSeccion: esEnUso && idPublicacion && idSeccion ? Number(idSeccion) : null, + obs: obs.trim() || null, + fechaCambioEstado, + }; + await onSubmit(bobinaActual.idBobina, dataToSubmit); + onClose(); + } catch (error: any) { + console.error("Error en submit de StockBobinaCambioEstadoModal:", error); + } finally { + setLoading(false); + } + }; + if (!bobinaActual) return null; - const validate = (): boolean => { - const errors: { [key: string]: string | null } = {}; - if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.'; - if (!fechaCambioEstado.trim()) errors.fechaCambioEstado = 'La fecha es obligatoria.'; - else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaCambioEstado)) errors.fechaCambioEstado = 'Formato de fecha inválido.'; - - if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { - if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.'; - if (!idSeccion) errors.idSeccion = 'Seleccione una sección.'; - } - setLocalErrors(errors); - return Object.keys(errors).length === 0; - }; - - const handleInputChange = (fieldName: string) => { - if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); - if (errorMessage) clearErrorMessage(); - // La lógica de limpieza de pub/secc se movió a un useEffect dedicado a nuevoEstadoId - // y el de sección a un useEffect de idPublicacion - if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion - setIdSeccion(''); - } - }; - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - clearErrorMessage(); - if (!validate() || !bobinaActual) return; - - setLoading(true); - try { - const esEnUso = Number(nuevoEstadoId) === ID_ESTADO_EN_USO; - const dataToSubmit: CambiarEstadoBobinaDto = { - nuevoEstadoId: Number(nuevoEstadoId), - idPublicacion: esEnUso && idPublicacion ? Number(idPublicacion) : null, - idSeccion: esEnUso && idPublicacion && idSeccion ? Number(idSeccion) : null, - obs: obs.trim() || null, - fechaCambioEstado, - }; - await onSubmit(bobinaActual.idBobina, dataToSubmit); - onClose(); - } catch (error: any) { - console.error("Error en submit de StockBobinaCambioEstadoModal:", error); - } finally { - setLoading(false); - } - }; - - if (!bobinaActual) return null; - - return ( - - - - Cambiar Estado de Bobina: {bobinaActual.nroBobina} - - - Estado Actual: {bobinaActual.nombreEstadoBobina} - - - - - Nuevo Estado - - {localErrors.nuevoEstadoId && {localErrors.nuevoEstadoId}} - - - {Number(nuevoEstadoId) === ID_ESTADO_EN_USO && ( - <> - - Publicación - { + setNuevoEstadoId(e.target.value as number | string); + handleInputChange('nuevoEstadoId'); + }} + disabled={loading || loadingDropdowns || estadosDisponibles.length === 0} > - Seleccione publicación - {publicacionesDisponibles.map((p) => ({p.nombre}))} + Seleccione un estado + {estadosDisponibles.map((e) => ({e.denominacion}))} - {localErrors.idPublicacion && {localErrors.idPublicacion}} + {localErrors.nuevoEstadoId && {localErrors.nuevoEstadoId}} - - Sección - - {localErrors.idSeccion && {localErrors.idSeccion}} - {localErrors.secciones && {localErrors.secciones}} - - - )} - {setFechaCambioEstado(e.target.value); handleInputChange('fechaCambioEstado');}} - margin="dense" fullWidth error={!!localErrors.fechaCambioEstado} helperText={localErrors.fechaCambioEstado || ''} - disabled={loading} InputLabelProps={{ shrink: true }} - /> - setObs(e.target.value)} - margin="dense" fullWidth multiline rows={3} disabled={loading} - /> + {Number(nuevoEstadoId) === ID_ESTADO_EN_USO && ( + <> + + Publicación + + {localErrors.idPublicacion && {localErrors.idPublicacion}} + + + Sección + + {localErrors.idSeccion && {localErrors.idSeccion}} + {localErrors.secciones && {localErrors.secciones}} + + + )} + + { setFechaCambioEstado(e.target.value); handleInputChange('fechaCambioEstado'); }} + margin="dense" + fullWidth + error={!!localErrors.fechaCambioEstado} + helperText={localErrors.fechaCambioEstado || ''} + disabled={loading} + InputLabelProps={{ shrink: true }} + inputProps={{ + min: bobinaActual?.fechaRemito.split('T')[0] + }} + /> + setObs(e.target.value)} + margin="dense" fullWidth multiline rows={3} disabled={loading} + /> + + + {errorMessage && {errorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + - - {errorMessage && {errorMessage}} - {localErrors.dropdowns && {localErrors.dropdowns}} - - - - - - - - - ); + + ); }; export default StockBobinaCambioEstadoModal; \ No newline at end of file