Backend API:
- Recargos por Zona (`dist_RecargoZona`):
- CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Endpoints anidados bajo `/publicaciones/{idPublicacion}/recargos`.
- Lógica de negocio para vigencias (cierre/reapertura de períodos).
- Auditoría en `dist_RecargoZona_H`.
- Porcentajes de Pago Distribuidores (`dist_PorcPago`):
- CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajespago`.
- Lógica de negocio para vigencias.
- Auditoría en `dist_PorcPago_H`.
- Porcentajes/Montos Pago Canillitas (`dist_PorcMonPagoCanilla`):
- CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajesmoncanilla`.
- Lógica de negocio para vigencias.
- Auditoría en `dist_PorcMonPagoCanilla_H`.
- Secciones de Publicación (`dist_dtPubliSecciones`):
- CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Endpoints anidados bajo `/publicaciones/{idPublicacion}/secciones`.
- Auditoría en `dist_dtPubliSecciones_H`.
- Entradas/Salidas Distribuidores (`dist_EntradasSalidas`):
- Implementado backend (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Lógica para determinar precios/recargos/porcentajes aplicables.
- Cálculo de monto y afectación de saldos de distribuidores en `cue_Saldos`.
- Auditoría en `dist_EntradasSalidas_H`.
- Correcciones de Mapeo Dapper:
- Aplicados alias explícitos en repositorios de RecargoZona, PorcPago, PorcMonCanilla, PubliSeccion,
Canilla, Distribuidor y Precio para asegurar mapeo correcto de IDs y columnas.
Frontend React:
- Recargos por Zona:
- `recargoZonaService.ts`.
- `RecargoZonaFormModal.tsx` para crear/editar períodos de recargos.
- `GestionarRecargosPublicacionPage.tsx` para listar y gestionar recargos por publicación.
- Porcentajes de Pago Distribuidores:
- `porcPagoService.ts`.
- `PorcPagoFormModal.tsx`.
- `GestionarPorcentajesPagoPage.tsx`.
- Porcentajes/Montos Pago Canillitas:
- `porcMonCanillaService.ts`.
- `PorcMonCanillaFormModal.tsx`.
- `GestionarPorcMonCanillaPage.tsx`.
- Secciones de Publicación:
- `publiSeccionService.ts`.
- `PubliSeccionFormModal.tsx`.
- `GestionarSeccionesPublicacionPage.tsx`.
- Navegación:
- Actualizadas rutas y menús para acceder a la gestión de recargos, porcentajes (dist. y canillita) y secciones desde la vista de una publicación.
- Layout:
- Uso consistente de `Box` con Flexbox en lugar de `Grid` en nuevos modales y páginas para evitar errores de tipo.
261 lines
13 KiB
TypeScript
261 lines
13 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 { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
|
import type { CambiarEstadoBobinaDto } from '../../../models/dtos/Impresion/CambiarEstadoBobinaDto';
|
|
import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto';
|
|
import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto';
|
|
import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto'; // Asumiendo que tienes este DTO
|
|
import estadoBobinaService from '../../../services/Impresion/estadoBobinaService'; // Para cargar estados
|
|
import publicacionService from '../../../services/Distribucion/publicacionService'; // Para cargar publicaciones
|
|
import publiSeccionService from '../../../services/Distribucion/publiSeccionService'; // Para cargar secciones
|
|
|
|
const modalStyle = { /* ... (mismo estilo) ... */
|
|
position: 'absolute' as 'absolute',
|
|
top: '50%',
|
|
left: '50%',
|
|
transform: 'translate(-50%, -50%)',
|
|
width: { xs: '90%', sm: 500 },
|
|
bgcolor: 'background.paper',
|
|
border: '2px solid #000',
|
|
boxShadow: 24,
|
|
p: 4,
|
|
maxHeight: '90vh',
|
|
overflowY: 'auto'
|
|
};
|
|
|
|
// IDs de estados conocidos (ajusta según tu BD)
|
|
const ID_ESTADO_EN_USO = 2;
|
|
const ID_ESTADO_DANADA = 3;
|
|
// const ID_ESTADO_DISPONIBLE = 1; // No se cambia a Disponible desde este modal
|
|
|
|
interface StockBobinaCambioEstadoModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise<void>;
|
|
bobinaActual: StockBobinaDto | null; // La bobina cuyo estado se va a cambiar
|
|
errorMessage?: string | null;
|
|
clearErrorMessage: () => void;
|
|
}
|
|
|
|
const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> = ({
|
|
open,
|
|
onClose,
|
|
onSubmit,
|
|
bobinaActual,
|
|
errorMessage,
|
|
clearErrorMessage
|
|
}) => {
|
|
const [nuevoEstadoId, setNuevoEstadoId] = useState<number | string>('');
|
|
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
|
|
const [idSeccion, setIdSeccion] = useState<number | string>('');
|
|
const [obs, setObs] = useState('');
|
|
const [fechaCambioEstado, setFechaCambioEstado] = useState('');
|
|
|
|
const [estadosDisponibles, setEstadosDisponibles] = useState<EstadoBobinaDto[]>([]);
|
|
const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDto[]>([]);
|
|
const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]);
|
|
|
|
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 estadosData = await estadoBobinaService.getAllEstadosBobina();
|
|
// Filtrar estados: no se puede volver a "Disponible" o al mismo estado actual desde aquí.
|
|
// Y si está "Dañada", no se puede cambiar.
|
|
let estadosFiltrados = estadosData.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina && e.idEstadoBobina !== 1);
|
|
if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) { // Si ya está dañada, no hay más cambios
|
|
estadosFiltrados = [];
|
|
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) { // Si está en uso, solo puede pasar a Dañada
|
|
estadosFiltrados = estadosData.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA);
|
|
}
|
|
|
|
|
|
setEstadosDisponibles(estadosFiltrados);
|
|
|
|
if (estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO)) { // Solo cargar publicaciones si "En Uso" es una opción
|
|
const publicacionesData = await publicacionService.getAllPublicaciones(undefined, undefined, true); // Solo habilitadas
|
|
setPublicacionesDisponibles(publicacionesData);
|
|
} else {
|
|
setPublicacionesDisponibles([]);
|
|
}
|
|
|
|
} 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('');
|
|
setIdPublicacion('');
|
|
setIdSeccion('');
|
|
setObs(bobinaActual.obs || ''); // Pre-cargar obs existente
|
|
setFechaCambioEstado(new Date().toISOString().split('T')[0]); // Default a hoy
|
|
setLocalErrors({});
|
|
clearErrorMessage();
|
|
}
|
|
}, [open, bobinaActual, clearErrorMessage]);
|
|
|
|
|
|
// Cargar secciones cuando cambia la publicación seleccionada y el estado es "En Uso"
|
|
useEffect(() => {
|
|
const fetchSecciones = async () => {
|
|
if (nuevoEstadoId === ID_ESTADO_EN_USO && idPublicacion) {
|
|
setLoadingDropdowns(true); // Podrías tener un loader específico para secciones
|
|
try {
|
|
const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true); // Solo activas
|
|
setSeccionesDisponibles(data);
|
|
} catch (error) {
|
|
console.error("Error al cargar secciones:", error);
|
|
setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.'}));
|
|
} finally {
|
|
setLoadingDropdowns(false);
|
|
}
|
|
} else {
|
|
setSeccionesDisponibles([]); // Limpiar secciones si no aplica
|
|
}
|
|
};
|
|
fetchSecciones();
|
|
}, [nuevoEstadoId, idPublicacion]);
|
|
|
|
|
|
const validate = (): boolean => {
|
|
const errors: { [key: string]: string | null } = {};
|
|
if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.';
|
|
if (!fechaCambioEstado.trim()) errors.fechaCambioEstado = 'La fecha de cambio 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();
|
|
if (fieldName === 'nuevoEstadoId') { // Si cambia el estado, resetear pub/secc
|
|
setIdPublicacion('');
|
|
setIdSeccion('');
|
|
}
|
|
if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion
|
|
setIdSeccion('');
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
clearErrorMessage();
|
|
if (!validate() || !bobinaActual) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const dataToSubmit: CambiarEstadoBobinaDto = {
|
|
nuevoEstadoId: Number(nuevoEstadoId),
|
|
idPublicacion: Number(nuevoEstadoId) === ID_ESTADO_EN_USO ? Number(idPublicacion) : null,
|
|
idSeccion: Number(nuevoEstadoId) === ID_ESTADO_EN_USO ? Number(idSeccion) : null,
|
|
obs: obs || undefined,
|
|
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 (
|
|
<Modal open={open} onClose={onClose}>
|
|
<Box sx={modalStyle}>
|
|
<Typography variant="h6" component="h2" gutterBottom>
|
|
Cambiar Estado de Bobina: {bobinaActual.nroBobina}
|
|
</Typography>
|
|
<Typography variant="body2" gutterBottom>
|
|
Estado Actual: <strong>{bobinaActual.nombreEstadoBobina}</strong>
|
|
</Typography>
|
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
|
<FormControl fullWidth margin="dense" error={!!localErrors.nuevoEstadoId}>
|
|
<InputLabel id="nuevo-estado-select-label" required>Nuevo Estado</InputLabel>
|
|
<Select labelId="nuevo-estado-select-label" label="Nuevo Estado" value={nuevoEstadoId}
|
|
onChange={(e) => {setNuevoEstadoId(e.target.value as number); handleInputChange('nuevoEstadoId');}}
|
|
disabled={loading || loadingDropdowns || estadosDisponibles.length === 0}
|
|
>
|
|
<MenuItem value="" disabled><em>Seleccione un estado</em></MenuItem>
|
|
{estadosDisponibles.map((e) => (<MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>))}
|
|
</Select>
|
|
{localErrors.nuevoEstadoId && <Typography color="error" variant="caption">{localErrors.nuevoEstadoId}</Typography>}
|
|
</FormControl>
|
|
|
|
{Number(nuevoEstadoId) === ID_ESTADO_EN_USO && (
|
|
<>
|
|
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion}>
|
|
<InputLabel id="publicacion-estado-select-label" required>Publicación</InputLabel>
|
|
<Select labelId="publicacion-estado-select-label" label="Publicación" value={idPublicacion}
|
|
onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}}
|
|
disabled={loading || loadingDropdowns}
|
|
>
|
|
<MenuItem value="" disabled><em>Seleccione publicación</em></MenuItem>
|
|
{publicacionesDisponibles.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>))}
|
|
</Select>
|
|
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
|
|
</FormControl>
|
|
<FormControl fullWidth margin="dense" error={!!localErrors.idSeccion}>
|
|
<InputLabel id="seccion-estado-select-label" required>Sección</InputLabel>
|
|
<Select labelId="seccion-estado-select-label" label="Sección" value={idSeccion}
|
|
onChange={(e) => {setIdSeccion(e.target.value as number); handleInputChange('idSeccion');}}
|
|
disabled={loading || loadingDropdowns || !idPublicacion || seccionesDisponibles.length === 0}
|
|
>
|
|
<MenuItem value="" disabled><em>{idPublicacion ? 'Seleccione sección' : 'Seleccione publicación primero'}</em></MenuItem>
|
|
{seccionesDisponibles.map((s) => (<MenuItem key={s.idSeccion} value={s.idSeccion}>{s.nombre}</MenuItem>))}
|
|
</Select>
|
|
{localErrors.idSeccion && <Typography color="error" variant="caption">{localErrors.idSeccion}</Typography>}
|
|
{localErrors.secciones && <Alert severity="warning" sx={{mt:0.5}}>{localErrors.secciones}</Alert>}
|
|
</FormControl>
|
|
</>
|
|
)}
|
|
|
|
<TextField label="Fecha Cambio de Estado" type="date" value={fechaCambioEstado} required
|
|
onChange={(e) => {setFechaCambioEstado(e.target.value); handleInputChange('fechaCambioEstado');}}
|
|
margin="dense" fullWidth error={!!localErrors.fechaCambioEstado} helperText={localErrors.fechaCambioEstado || ''}
|
|
disabled={loading} InputLabelProps={{ shrink: true }}
|
|
/>
|
|
<TextField label="Observaciones (Opcional)" value={obs}
|
|
onChange={(e) => setObs(e.target.value)}
|
|
margin="dense" fullWidth multiline rows={3} disabled={loading}
|
|
/>
|
|
</Box>
|
|
|
|
{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 || estadosDisponibles.length === 0}>
|
|
{loading ? <CircularProgress size={24} /> : 'Guardar Cambio de Estado'}
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default StockBobinaCambioEstadoModal; |