feat: Implementación de Secciones, Recargos, Porc. Pago Dist. y backend E/S Dist.

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.
This commit is contained in:
2025-05-21 14:58:52 -03:00
parent b6ba52f074
commit e7e185a9cb
140 changed files with 10465 additions and 394 deletions

View File

@@ -0,0 +1,261 @@
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;

View File

@@ -0,0 +1,206 @@
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 { UpdateStockBobinaDto } from '../../../models/dtos/Impresion/UpdateStockBobinaDto';
import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto';
import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto';
import tipoBobinaService from '../../../services/Impresion/tipoBobinaService';
import plantaService from '../../../services/Impresion/plantaService';
const modalStyle = { /* ... (mismo estilo que StockBobinaIngresoFormModal) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 550 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
interface StockBobinaEditFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (idBobina: number, data: UpdateStockBobinaDto) => Promise<void>;
initialData: StockBobinaDto | null; // Siempre habrá initialData para editar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const StockBobinaEditFormModal: React.FC<StockBobinaEditFormModalProps> = ({
open,
onClose,
onSubmit,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idTipoBobina, setIdTipoBobina] = useState<number | string>('');
const [nroBobina, setNroBobina] = useState('');
const [peso, setPeso] = useState<string>('');
const [idPlanta, setIdPlanta] = useState<number | string>('');
const [remito, setRemito] = useState('');
const [fechaRemito, setFechaRemito] = useState(''); // yyyy-MM-dd
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
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 [tiposData, plantasData] = await Promise.all([
tipoBobinaService.getAllTiposBobina(),
plantaService.getAllPlantas()
]);
setTiposBobina(tiposData);
setPlantas(plantasData);
} catch (error) {
console.error("Error al cargar datos para dropdowns (StockBobina Edit)", error);
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar tipos/plantas.'}));
} finally {
setLoadingDropdowns(false);
}
};
if (open && initialData) {
fetchDropdownData();
setIdTipoBobina(initialData.idTipoBobina || '');
setNroBobina(initialData.nroBobina || '');
setPeso(initialData.peso?.toString() || '');
setIdPlanta(initialData.idPlanta || '');
setRemito(initialData.remito || '');
setFechaRemito(initialData.fechaRemito || ''); // Asume yyyy-MM-dd del DTO
setLocalErrors({});
clearErrorMessage();
}
}, [open, initialData, clearErrorMessage]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idTipoBobina) errors.idTipoBobina = 'Seleccione un tipo.';
if (!nroBobina.trim()) errors.nroBobina = 'Nro. Bobina es obligatorio.';
if (!peso.trim() || isNaN(parseInt(peso)) || parseInt(peso) <= 0) errors.peso = 'Peso debe ser un número positivo.';
if (!idPlanta) errors.idPlanta = 'Seleccione una planta.';
if (!remito.trim()) errors.remito = 'Remito es obligatorio.';
if (!fechaRemito.trim()) errors.fechaRemito = 'Fecha de Remito es obligatoria.';
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaRemito)) errors.fechaRemito = 'Formato de fecha inválido.';
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() || !initialData) return; // initialData siempre debería existir aquí
setLoading(true);
try {
const dataToSubmit: UpdateStockBobinaDto = {
idTipoBobina: Number(idTipoBobina),
nroBobina,
peso: parseInt(peso, 10),
idPlanta: Number(idPlanta),
remito,
fechaRemito,
};
await onSubmit(initialData.idBobina, dataToSubmit);
onClose();
} catch (error: any) {
console.error("Error en submit de StockBobinaEditFormModal:", error);
// El error de API lo maneja la página que llama a este modal
} finally {
setLoading(false);
}
};
if (!initialData) return null; // No renderizar si no hay datos iniciales (aunque open lo controla)
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
Editar Datos de Bobina (ID: {initialData.idBobina})
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idTipoBobina} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="edit-tipo-bobina-select-label" required>Tipo Bobina</InputLabel>
<Select labelId="edit-tipo-bobina-select-label" label="Tipo Bobina" value={idTipoBobina}
onChange={(e) => {setIdTipoBobina(e.target.value as number); handleInputChange('idTipoBobina');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione un tipo</em></MenuItem>
{tiposBobina.map((t) => (<MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>))}
</Select>
{localErrors.idTipoBobina && <Typography color="error" variant="caption">{localErrors.idTipoBobina}</Typography>}
</FormControl>
<TextField label="Nro. Bobina" value={nroBobina} required
onChange={(e) => {setNroBobina(e.target.value); handleInputChange('nroBobina');}}
margin="dense" fullWidth error={!!localErrors.nroBobina} helperText={localErrors.nroBobina || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}} autoFocus
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Peso (Kg)" type="number" value={peso} required
onChange={(e) => {setPeso(e.target.value); handleInputChange('peso');}}
margin="dense" fullWidth error={!!localErrors.peso} helperText={localErrors.peso || ''}
disabled={loading} sx={{flex:1, minWidth: '150px'}}
/>
<FormControl fullWidth margin="dense" error={!!localErrors.idPlanta} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="edit-planta-select-label" required>Planta Destino</InputLabel>
<Select labelId="edit-planta-select-label" label="Planta Destino" value={idPlanta}
onChange={(e) => {setIdPlanta(e.target.value as number); handleInputChange('idPlanta');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione una planta</em></MenuItem>
{plantas.map((p) => (<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>))}
</Select>
{localErrors.idPlanta && <Typography color="error" variant="caption">{localErrors.idPlanta}</Typography>}
</FormControl>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Nro. Remito" value={remito} required
onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}}
margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}}
/>
<TextField label="Fecha Remito" type="date" value={fechaRemito} required
onChange={(e) => {setFechaRemito(e.target.value); handleInputChange('fechaRemito');}}
margin="dense" fullWidth error={!!localErrors.fechaRemito} helperText={localErrors.fechaRemito || ''}
disabled={loading} InputLabelProps={{ shrink: true }} sx={{flex:1, minWidth: '200px'}}
/>
</Box>
</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}>
{loading ? <CircularProgress size={24} /> : 'Guardar Cambios'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default StockBobinaEditFormModal;

View File

@@ -0,0 +1,192 @@
import React, { useState, useEffect } from 'react';
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem } from '@mui/material';
import type { CreateStockBobinaDto } from '../../../models/dtos/Impresion/CreateStockBobinaDto';
import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto';
import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto';
import tipoBobinaService from '../../../services/Impresion/tipoBobinaService';
import plantaService from '../../../services/Impresion/plantaService';
const modalStyle = { /* ... (mismo estilo) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 550 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
interface StockBobinaIngresoFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateStockBobinaDto) => Promise<void>; // Solo para crear
errorMessage?: string | null;
clearErrorMessage: () => void;
// initialData no es necesario para un modal de solo creación
}
const StockBobinaIngresoFormModal: React.FC<StockBobinaIngresoFormModalProps> = ({
open,
onClose,
onSubmit,
errorMessage,
clearErrorMessage
}) => {
const [idTipoBobina, setIdTipoBobina] = useState<number | string>('');
const [nroBobina, setNroBobina] = useState('');
const [peso, setPeso] = useState<string>('');
const [idPlanta, setIdPlanta] = useState<number | string>('');
const [remito, setRemito] = useState('');
const [fechaRemito, setFechaRemito] = useState(''); // yyyy-MM-dd
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
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 [tiposData, plantasData] = await Promise.all([
tipoBobinaService.getAllTiposBobina(),
plantaService.getAllPlantas()
]);
setTiposBobina(tiposData);
setPlantas(plantasData);
} catch (error) {
console.error("Error al cargar datos para dropdowns (StockBobina)", error);
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar tipos/plantas.'}));
} finally {
setLoadingDropdowns(false);
}
};
if (open) {
fetchDropdownData();
// Resetear campos
setIdTipoBobina(''); setNroBobina(''); setPeso(''); setIdPlanta('');
setRemito(''); setFechaRemito(new Date().toISOString().split('T')[0]); // Default a hoy
setLocalErrors({});
clearErrorMessage();
}
}, [open, clearErrorMessage]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idTipoBobina) errors.idTipoBobina = 'Seleccione un tipo.';
if (!nroBobina.trim()) errors.nroBobina = 'Nro. Bobina es obligatorio.';
if (!peso.trim() || isNaN(parseInt(peso)) || parseInt(peso) <= 0) errors.peso = 'Peso debe ser un número positivo.';
if (!idPlanta) errors.idPlanta = 'Seleccione una planta.';
if (!remito.trim()) errors.remito = 'Remito es obligatorio.';
if (!fechaRemito.trim()) errors.fechaRemito = 'Fecha de Remito es obligatoria.';
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaRemito)) errors.fechaRemito = 'Formato de fecha inválido.';
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;
setLoading(true);
try {
const dataToSubmit: CreateStockBobinaDto = {
idTipoBobina: Number(idTipoBobina),
nroBobina,
peso: parseInt(peso, 10),
idPlanta: Number(idPlanta),
remito,
fechaRemito,
};
await onSubmit(dataToSubmit);
onClose();
} catch (error: any) {
console.error("Error en submit de StockBobinaIngresoFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>Ingresar Nueva Bobina a Stock</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idTipoBobina} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="tipo-bobina-select-label" required>Tipo Bobina</InputLabel>
<Select labelId="tipo-bobina-select-label" label="Tipo Bobina" value={idTipoBobina}
onChange={(e) => {setIdTipoBobina(e.target.value as number); handleInputChange('idTipoBobina');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione un tipo</em></MenuItem>
{tiposBobina.map((t) => (<MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>))}
</Select>
{localErrors.idTipoBobina && <Typography color="error" variant="caption">{localErrors.idTipoBobina}</Typography>}
</FormControl>
<TextField label="Nro. Bobina" value={nroBobina} required
onChange={(e) => {setNroBobina(e.target.value); handleInputChange('nroBobina');}}
margin="dense" fullWidth error={!!localErrors.nroBobina} helperText={localErrors.nroBobina || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Peso (Kg)" type="number" value={peso} required
onChange={(e) => {setPeso(e.target.value); handleInputChange('peso');}}
margin="dense" fullWidth error={!!localErrors.peso} helperText={localErrors.peso || ''}
disabled={loading} sx={{flex:1, minWidth: '150px'}}
/>
<FormControl fullWidth margin="dense" error={!!localErrors.idPlanta} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="planta-select-label" required>Planta Destino</InputLabel>
<Select labelId="planta-select-label" label="Planta Destino" value={idPlanta}
onChange={(e) => {setIdPlanta(e.target.value as number); handleInputChange('idPlanta');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione una planta</em></MenuItem>
{plantas.map((p) => (<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>))}
</Select>
{localErrors.idPlanta && <Typography color="error" variant="caption">{localErrors.idPlanta}</Typography>}
</FormControl>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Nro. Remito" value={remito} required
onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}}
margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}}
/>
<TextField label="Fecha Remito" type="date" value={fechaRemito} required
onChange={(e) => {setFechaRemito(e.target.value); handleInputChange('fechaRemito');}}
margin="dense" fullWidth error={!!localErrors.fechaRemito} helperText={localErrors.fechaRemito || ''}
disabled={loading} InputLabelProps={{ shrink: true }} sx={{flex:1, minWidth: '200px'}}
/>
</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}>
{loading ? <CircularProgress size={24} /> : 'Ingresar Bobina'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default StockBobinaIngresoFormModal;

View File

@@ -0,0 +1,334 @@
// src/components/Modals/TiradaFormModal.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, IconButton, Paper,
Table, TableHead, TableRow, TableCell, TableBody,
TableContainer
} from '@mui/material';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import type { CreateTiradaRequestDto } from '../../../models/dtos/Impresion/CreateTiradaRequestDto';
import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto';
import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto';
import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto';
import publicacionService from '../../../services/Distribucion/publicacionService';
import plantaService from '../../../services/Impresion/plantaService';
import publiSeccionService from '../../../services/Distribucion/publiSeccionService';
const modalStyle = { /* ... (mismo estilo) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '95%', sm: '80%', md: '750px' },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 3,
maxHeight: '90vh',
overflowY: 'auto'
};
// CORREGIDO: Ajustar el tipo para los inputs. Usaremos string para los inputs,
// y convertiremos a number al hacer submit o al validar donde sea necesario.
interface DetalleSeccionFormState {
idSeccion: number | ''; // Permitir string vacío para el Select no seleccionado
nombreSeccion?: string;
cantPag: string; // TextField de cantPag siempre es string
idTemporal: string; // Para la key de React
}
interface TiradaFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateTiradaRequestDto) => Promise<void>;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const TiradaFormModal: React.FC<TiradaFormModalProps> = ({
open,
onClose,
onSubmit,
errorMessage,
clearErrorMessage
}) => {
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [idPlanta, setIdPlanta] = useState<number | string>('');
const [ejemplares, setEjemplares] = useState<string>('');
// CORREGIDO: Usar el nuevo tipo para el estado del formulario de secciones
const [seccionesDeTirada, setSeccionesDeTirada] = useState<DetalleSeccionFormState[]>([]);
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [seccionesPublicacion, setSeccionesPublicacion] = useState<PubliSeccionDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const resetForm = () => {
setIdPublicacion('');
setFecha(new Date().toISOString().split('T')[0]);
setIdPlanta('');
setEjemplares('');
setSeccionesDeTirada([]);
setSeccionesPublicacion([]);
setLocalErrors({});
clearErrorMessage();
};
const fetchInitialDropdowns = useCallback(async () => {
setLoadingDropdowns(true);
try {
const [pubsData, plantasData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true),
plantaService.getAllPlantas()
]);
setPublicaciones(pubsData);
setPlantas(plantasData);
} catch (error) {
console.error("Error al cargar publicaciones/plantas", error);
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos iniciales.'}));
} finally {
setLoadingDropdowns(false);
}
}, []);
useEffect(() => {
if (open) {
resetForm(); // Llama a resetForm aquí
fetchInitialDropdowns();
}
}, [open, fetchInitialDropdowns]); // resetForm no necesita estar en las dependencias si su contenido no cambia basado en props/estado que también estén en las dependencias.
const fetchSeccionesDePublicacion = useCallback(async (pubId: number) => {
if (!pubId) {
setSeccionesPublicacion([]);
setSeccionesDeTirada([]);
return;
}
setLoadingDropdowns(true);
try {
const data = await publiSeccionService.getSeccionesPorPublicacion(pubId, true);
setSeccionesPublicacion(data);
setSeccionesDeTirada([]);
} catch (error) {
console.error("Error al cargar secciones de la publicación", error);
setLocalErrors(prev => ({...prev, secciones: 'Error al cargar secciones.'}));
} finally {
setLoadingDropdowns(false);
}
}, []);
useEffect(() => {
if (idPublicacion) {
fetchSeccionesDePublicacion(Number(idPublicacion));
} else {
setSeccionesPublicacion([]);
setSeccionesDeTirada([]);
}
}, [idPublicacion, fetchSeccionesDePublicacion]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.';
if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.';
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido.';
if (!idPlanta) errors.idPlanta = 'Seleccione una planta.';
if (!ejemplares.trim() || isNaN(parseInt(ejemplares)) || parseInt(ejemplares) <= 0) errors.ejemplares = 'Ejemplares debe ser un número positivo.';
if (seccionesDeTirada.length === 0) {
errors.seccionesArray = 'Debe agregar al menos una sección a la tirada.';
} else {
seccionesDeTirada.forEach((sec, index) => {
if (sec.idSeccion === '') errors[`seccion_${index}_id`] = `Fila ${index + 1}: Debe seleccionar una sección.`;
if (!sec.cantPag.trim() || isNaN(Number(sec.cantPag)) || Number(sec.cantPag) <= 0) {
errors[`seccion_${index}_pag`] = `Fila ${index + 1}: Cant. Páginas debe ser un número positivo.`;
}
});
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleAddSeccion = () => {
setSeccionesDeTirada([...seccionesDeTirada, { idSeccion: '', cantPag: '', nombreSeccion: '', idTemporal: crypto.randomUUID() }]);
if (localErrors.seccionesArray) setLocalErrors(prev => ({ ...prev, seccionesArray: null }));
};
const handleRemoveSeccion = (index: number) => {
setSeccionesDeTirada(seccionesDeTirada.filter((_, i) => i !== index));
};
const handleSeccionChange = (index: number, field: 'idSeccion' | 'cantPag', value: string | number) => {
const nuevasSecciones = [...seccionesDeTirada];
const targetSeccion = nuevasSecciones[index];
if (field === 'idSeccion') {
const numValue = Number(value); // El valor del Select es string, pero lo guardamos como number | ''
targetSeccion.idSeccion = numValue === 0 ? '' : numValue; // Si es 0 (placeholder), guardar ''
const seccionSeleccionada = seccionesPublicacion.find(s => s.idSeccion === numValue);
targetSeccion.nombreSeccion = seccionSeleccionada?.nombre || '';
} else { // cantPag
targetSeccion.cantPag = value as string; // Guardar como string, validar como número después
}
setSeccionesDeTirada(nuevasSecciones);
if (localErrors[`seccion_${index}_id`]) setLocalErrors(prev => ({ ...prev, [`seccion_${index}_id`]: null }));
if (localErrors[`seccion_${index}_pag`]) setLocalErrors(prev => ({ ...prev, [`seccion_${index}_pag`]: null }));
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
try {
const dataToSubmit: CreateTiradaRequestDto = {
idPublicacion: Number(idPublicacion),
fecha,
idPlanta: Number(idPlanta),
ejemplares: parseInt(ejemplares, 10),
// CORREGIDO: Asegurar que los datos de secciones sean números
secciones: seccionesDeTirada.map(s => ({
idSeccion: Number(s.idSeccion), // Convertir a número aquí
cantPag: Number(s.cantPag) // Convertir a número aquí
}))
};
await onSubmit(dataToSubmit);
onClose();
} catch (error: any) {
console.error("Error en submit de TiradaFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>Registrar Nueva Tirada</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
{/* ... (campos de Publicacion, Fecha, Planta, Ejemplares sin cambios) ... */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} sx={{flex:1, minWidth: 200}}>
<InputLabel id="publicacion-tirada-select-label" required>Publicación</InputLabel>
<Select labelId="publicacion-tirada-select-label" label="Publicación" value={idPublicacion}
onChange={(e) => { setIdPublicacion(e.target.value as number); setLocalErrors(p => ({...p, idPublicacion: null})); }}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{publicaciones.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre} ({p.nombreEmpresa})</MenuItem>))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPublicacion}</Typography>}
</FormControl>
<TextField label="Fecha Tirada" type="date" value={fecha} required
onChange={(e) => {setFecha(e.target.value); setLocalErrors(p => ({...p, fecha: null}));}}
margin="dense" error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
disabled={loading} InputLabelProps={{ shrink: true }} sx={{flex:1, minWidth: 160}}
/>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idPlanta} sx={{flex:1, minWidth: 200}}>
<InputLabel id="planta-tirada-select-label" required>Planta</InputLabel>
<Select labelId="planta-tirada-select-label" label="Planta" value={idPlanta}
onChange={(e) => {setIdPlanta(e.target.value as number); setLocalErrors(p => ({...p, idPlanta: null}));}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{plantas.map((p) => (<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>))}
</Select>
{localErrors.idPlanta && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPlanta}</Typography>}
</FormControl>
<TextField label="Total Ejemplares" type="number" value={ejemplares} required
onChange={(e) => {setEjemplares(e.target.value); setLocalErrors(p => ({...p, ejemplares: null}));}}
margin="dense" error={!!localErrors.ejemplares} helperText={localErrors.ejemplares || ''}
disabled={loading} sx={{flex:1, minWidth: 150}}
inputProps={{min:1}}
/>
</Box>
<Typography variant="subtitle1" sx={{mt: 2, mb:1}}>Detalle de Secciones Impresas:</Typography>
{localErrors.seccionesArray && <Alert severity="error" sx={{mb:1}}>{localErrors.seccionesArray}</Alert>}
<Paper variant="outlined" sx={{p:1, mb:2, maxHeight: '250px', overflowY: 'auto'}}> {/* Permitir scroll en tabla de secciones */}
<TableContainer>
<Table size="small" stickyHeader> {/* stickyHeader para que cabecera quede fija */}
<TableHead>
<TableRow>
<TableCell sx={{fontWeight:'bold', minWidth: 200}}>Sección</TableCell>
<TableCell sx={{fontWeight:'bold', width: '150px'}}>Cant. Páginas</TableCell>
<TableCell align="right" sx={{width: '50px'}}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{seccionesDeTirada.map((sec, index) => (
<TableRow key={sec.idTemporal || index}> {/* Usar idTemporal para key */}
<TableCell sx={{py:0.5}}>
<FormControl fullWidth size="small" error={!!localErrors[`seccion_${index}_id`]}>
<Select value={sec.idSeccion} // Ahora idSeccion es number | ''
onChange={(e) => handleSeccionChange(index, 'idSeccion', e.target.value as number | '')}
disabled={loading || loadingDropdowns || seccionesPublicacion.length === 0}
displayEmpty
>
<MenuItem value="" disabled><em>Seleccionar</em></MenuItem>
{seccionesPublicacion.map(s => <MenuItem key={s.idSeccion} value={s.idSeccion}>{s.nombre}</MenuItem>)}
</Select>
{localErrors[`seccion_${index}_id`] && <Typography color="error" variant="caption">{localErrors[`seccion_${index}_id`]}</Typography>}
</FormControl>
</TableCell>
<TableCell sx={{py:0.5}}>
<TextField type="number" size="small" fullWidth value={sec.cantPag}
onChange={(e) => handleSeccionChange(index, 'cantPag', e.target.value)}
error={!!localErrors[`seccion_${index}_pag`]}
helperText={localErrors[`seccion_${index}_pag`] || ''}
disabled={loading}
inputProps={{min:1}}
/>
</TableCell>
<TableCell align="right" sx={{py:0.5}}>
<IconButton onClick={() => handleRemoveSeccion(index)} size="small" color="error" disabled={loading}>
<DeleteOutlineIcon />
</IconButton>
</TableCell>
</TableRow>
))}
{seccionesDeTirada.length === 0 && (
<TableRow>
<TableCell colSpan={3} align="center">
<Typography variant="caption" color="textSecondary">
Agregue secciones a la tirada.
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<Button startIcon={<AddCircleOutlineIcon />} onClick={handleAddSeccion} sx={{mt:1}} size="small" disabled={loading || !idPublicacion}>
Agregar Sección
</Button>
</Paper>
{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} /> : 'Registrar Tirada'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default TiradaFormModal;