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:
@@ -0,0 +1,227 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, RadioGroup, FormControlLabel, Radio
|
||||
} from '@mui/material';
|
||||
import type { EntradaSalidaDistDto } from '../../../models/dtos/Distribucion/EntradaSalidaDistDto';
|
||||
import type { CreateEntradaSalidaDistDto } from '../../../models/dtos/Distribucion/CreateEntradaSalidaDistDto';
|
||||
import type { UpdateEntradaSalidaDistDto } from '../../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto';
|
||||
import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { DistribuidorDto } from '../../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import publicacionService from '../../../services/Distribucion/publicacionService';
|
||||
import distribuidorService from '../../../services/Distribucion/distribuidorService';
|
||||
|
||||
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 EntradaSalidaDistFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => Promise<void>;
|
||||
initialData?: EntradaSalidaDistDto | null; // Para editar
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
|
||||
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
|
||||
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [tipoMovimiento, setTipoMovimiento] = useState<'Salida' | 'Entrada'>('Salida');
|
||||
const [cantidad, setCantidad] = useState<string>('');
|
||||
const [remito, setRemito] = useState<string>('');
|
||||
const [observacion, setObservacion] = useState('');
|
||||
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDropdownData = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [pubsData, distData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true), // Solo habilitadas
|
||||
distribuidorService.getAllDistribuidores()
|
||||
]);
|
||||
setPublicaciones(pubsData);
|
||||
setDistribuidores(distData);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar datos para dropdowns", error);
|
||||
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'}));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchDropdownData();
|
||||
setIdPublicacion(initialData?.idPublicacion || '');
|
||||
setIdDistribuidor(initialData?.idDistribuidor || '');
|
||||
setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]);
|
||||
setTipoMovimiento(initialData?.tipoMovimiento || 'Salida');
|
||||
setCantidad(initialData?.cantidad?.toString() || '');
|
||||
setRemito(initialData?.remito?.toString() || '');
|
||||
setObservacion(initialData?.observacion || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.';
|
||||
if (!idDistribuidor) errors.idDistribuidor = 'Seleccione un distribuidor.';
|
||||
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 (!tipoMovimiento) errors.tipoMovimiento = 'Seleccione un tipo de movimiento.';
|
||||
if (!cantidad.trim() || isNaN(parseInt(cantidad)) || parseInt(cantidad) <= 0) {
|
||||
errors.cantidad = 'La cantidad debe ser un número positivo.';
|
||||
}
|
||||
if (!isEditing && (!remito.trim() || isNaN(parseInt(remito)) || parseInt(remito) <= 0)) {
|
||||
errors.remito = 'El Nro. Remito es obligatorio y debe ser un número positivo.';
|
||||
}
|
||||
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 {
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdateEntradaSalidaDistDto = {
|
||||
cantidad: parseInt(cantidad, 10),
|
||||
observacion: observacion || undefined,
|
||||
};
|
||||
await onSubmit(dataToSubmit, initialData.idParte);
|
||||
} else {
|
||||
const dataToSubmit: CreateEntradaSalidaDistDto = {
|
||||
idPublicacion: Number(idPublicacion),
|
||||
idDistribuidor: Number(idDistribuidor),
|
||||
fecha,
|
||||
tipoMovimiento,
|
||||
cantidad: parseInt(cantidad, 10),
|
||||
remito: parseInt(remito, 10),
|
||||
observacion: observacion || undefined,
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de EntradaSalidaDistFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Movimiento Distribuidor' : 'Registrar Movimiento Distribuidor'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
|
||||
<InputLabel id="publicacion-esd-select-label">Publicación</InputLabel>
|
||||
<Select labelId="publicacion-esd-select-label" label="Publicación" value={idPublicacion}
|
||||
onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}}
|
||||
disabled={loading || loadingDropdowns || isEditing}
|
||||
>
|
||||
<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">{localErrors.idPublicacion}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idDistribuidor} required>
|
||||
<InputLabel id="distribuidor-esd-select-label">Distribuidor</InputLabel>
|
||||
<Select labelId="distribuidor-esd-select-label" label="Distribuidor" value={idDistribuidor}
|
||||
onChange={(e) => {setIdDistribuidor(e.target.value as number); handleInputChange('idDistribuidor');}}
|
||||
disabled={loading || loadingDropdowns || isEditing}
|
||||
>
|
||||
<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>
|
||||
|
||||
<TextField label="Fecha Movimiento" type="date" value={fecha} required
|
||||
onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}}
|
||||
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
|
||||
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
|
||||
/>
|
||||
|
||||
<FormControl component="fieldset" margin="dense" error={!!localErrors.tipoMovimiento} required>
|
||||
<Typography component="legend" variant="body2" sx={{mb:0.5}}>Tipo de Movimiento</Typography>
|
||||
<RadioGroup row value={tipoMovimiento} onChange={(e) => {setTipoMovimiento(e.target.value as 'Salida' | 'Entrada'); handleInputChange('tipoMovimiento');}} >
|
||||
<FormControlLabel value="Salida" control={<Radio size="small"/>} label="Salida (a Distribuidor)" disabled={loading || isEditing}/>
|
||||
<FormControlLabel value="Entrada" control={<Radio size="small"/>} label="Entrada (de Distribuidor)" disabled={loading || isEditing}/>
|
||||
</RadioGroup>
|
||||
{localErrors.tipoMovimiento && <Typography color="error" variant="caption">{localErrors.tipoMovimiento}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<TextField label="Nro. Remito" type="number" value={remito} required={!isEditing}
|
||||
onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}}
|
||||
margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''}
|
||||
disabled={loading || isEditing} inputProps={{min:1}}
|
||||
/>
|
||||
|
||||
<TextField label="Cantidad" type="number" value={cantidad} required
|
||||
onChange={(e) => {setCantidad(e.target.value); handleInputChange('cantidad');}}
|
||||
margin="dense" fullWidth error={!!localErrors.cantidad} helperText={localErrors.cantidad || ''}
|
||||
disabled={loading} inputProps={{min:1}}
|
||||
/>
|
||||
<TextField label="Observación (Opcional)" value={observacion}
|
||||
onChange={(e) => setObservacion(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={2} 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}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Movimiento')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntradaSalidaDistFormModal;
|
||||
@@ -0,0 +1,209 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox, InputAdornment
|
||||
} from '@mui/material';
|
||||
import type { PorcMonCanillaDto } from '../../../models/dtos/Distribucion/PorcMonCanillaDto';
|
||||
import type { CreatePorcMonCanillaDto } from '../../../models/dtos/Distribucion/CreatePorcMonCanillaDto';
|
||||
import type { UpdatePorcMonCanillaDto } from '../../../models/dtos/Distribucion/UpdatePorcMonCanillaDto';
|
||||
import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; // Para el dropdown
|
||||
import canillaService from '../../../services/Distribucion/canillaService'; // Para cargar canillitas
|
||||
|
||||
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 PorcMonCanillaFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreatePorcMonCanillaDto | UpdatePorcMonCanillaDto, idPorcMon?: number) => Promise<void>;
|
||||
idPublicacion: number;
|
||||
initialData?: PorcMonCanillaDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const PorcMonCanillaFormModal: React.FC<PorcMonCanillaFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
idPublicacion,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [idCanilla, setIdCanilla] = useState<number | string>('');
|
||||
const [vigenciaD, setVigenciaD] = useState('');
|
||||
const [vigenciaH, setVigenciaH] = useState('');
|
||||
const [porcMon, setPorcMon] = useState<string>('');
|
||||
const [esPorcentaje, setEsPorcentaje] = useState(true); // Default a porcentaje
|
||||
|
||||
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingCanillitas, setLoadingCanillitas] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCanillitas = async () => {
|
||||
setLoadingCanillitas(true);
|
||||
try {
|
||||
// Aquí podríamos querer filtrar solo canillitas accionistas si la regla de negocio lo impone
|
||||
// o todos los activos. Por ahora, todos los activos.
|
||||
const data = await canillaService.getAllCanillas(undefined, undefined, true);
|
||||
setCanillitas(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar canillitas", error);
|
||||
setLocalErrors(prev => ({...prev, canillitas: 'Error al cargar canillitas.'}));
|
||||
} finally {
|
||||
setLoadingCanillitas(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchCanillitas();
|
||||
setIdCanilla(initialData?.idCanilla || '');
|
||||
setVigenciaD(initialData?.vigenciaD || '');
|
||||
setVigenciaH(initialData?.vigenciaH || '');
|
||||
setPorcMon(initialData?.porcMon?.toString() || '');
|
||||
setEsPorcentaje(initialData ? initialData.esPorcentaje : true);
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!idCanilla) errors.idCanilla = 'Debe seleccionar un canillita.';
|
||||
if (!isEditing && !vigenciaD.trim()) {
|
||||
errors.vigenciaD = 'La Vigencia Desde es obligatoria.';
|
||||
} else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) {
|
||||
errors.vigenciaD = 'Formato de Vigencia Desde inválido (YYYY-MM-DD).';
|
||||
}
|
||||
if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) {
|
||||
errors.vigenciaH = 'Formato de Vigencia Hasta inválido (YYYY-MM-DD).';
|
||||
} else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) {
|
||||
errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.';
|
||||
}
|
||||
if (!porcMon.trim()) errors.porcMon = 'El valor es obligatorio.';
|
||||
else {
|
||||
const numVal = parseFloat(porcMon);
|
||||
if (isNaN(numVal) || numVal < 0) {
|
||||
errors.porcMon = 'El valor debe ser un número positivo.';
|
||||
} else if (esPorcentaje && numVal > 100) {
|
||||
errors.porcMon = 'El porcentaje no puede ser mayor a 100.';
|
||||
}
|
||||
}
|
||||
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 valorNum = parseFloat(porcMon);
|
||||
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdatePorcMonCanillaDto = {
|
||||
porcMon: valorNum,
|
||||
esPorcentaje,
|
||||
vigenciaH: vigenciaH.trim() ? vigenciaH : null,
|
||||
};
|
||||
await onSubmit(dataToSubmit, initialData.idPorcMon);
|
||||
} else {
|
||||
const dataToSubmit: CreatePorcMonCanillaDto = {
|
||||
idPublicacion,
|
||||
idCanilla: Number(idCanilla),
|
||||
vigenciaD,
|
||||
porcMon: valorNum,
|
||||
esPorcentaje,
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de PorcMonCanillaFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Porcentaje/Monto Canillita' : 'Agregar Nuevo Porcentaje/Monto Canillita'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idCanilla}>
|
||||
<InputLabel id="canilla-pmc-select-label" required>Canillita</InputLabel>
|
||||
<Select labelId="canilla-pmc-select-label" label="Canillita" value={idCanilla}
|
||||
onChange={(e) => {setIdCanilla(e.target.value as number); handleInputChange('idCanilla');}}
|
||||
disabled={loading || loadingCanillitas || isEditing}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione un canillita</em></MenuItem>
|
||||
{canillitas.map((c) => (<MenuItem key={c.idCanilla} value={c.idCanilla}>{`${c.nomApe} (Leg: ${c.legajo || 'S/L'})`}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idCanilla && <Typography color="error" variant="caption">{localErrors.idCanilla}</Typography>}
|
||||
</FormControl>
|
||||
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing}
|
||||
onChange={(e) => {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}}
|
||||
margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
|
||||
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
|
||||
/>
|
||||
{isEditing && (
|
||||
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH}
|
||||
onChange={(e) => {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}}
|
||||
margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''}
|
||||
disabled={loading} InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
)}
|
||||
<TextField label="Valor" type="number" value={porcMon} required
|
||||
onChange={(e) => {setPorcMon(e.target.value); handleInputChange('porcMon');}}
|
||||
margin="dense" fullWidth error={!!localErrors.porcMon} helperText={localErrors.porcMon || ''}
|
||||
disabled={loading}
|
||||
InputProps={{ startAdornment: esPorcentaje ? undefined : <InputAdornment position="start">$</InputAdornment>,
|
||||
endAdornment: esPorcentaje ? <InputAdornment position="end">%</InputAdornment> : undefined }}
|
||||
inputProps={{ step: "0.01", lang:"es-AR" }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={esPorcentaje} onChange={(e) => setEsPorcentaje(e.target.checked)} disabled={loading}/>}
|
||||
label="Es Porcentaje (si no, es Monto Fijo)" sx={{mt:1}}
|
||||
/>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.canillitas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.canillitas}</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 || loadingCanillitas}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PorcMonCanillaFormModal;
|
||||
@@ -0,0 +1,196 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, InputAdornment
|
||||
} from '@mui/material';
|
||||
import type { PorcPagoDto } from '../../../models/dtos/Distribucion/PorcPagoDto';
|
||||
import type { CreatePorcPagoDto } from '../../../models/dtos/Distribucion/CreatePorcPagoDto';
|
||||
import type { UpdatePorcPagoDto } from '../../../models/dtos/Distribucion/UpdatePorcPagoDto';
|
||||
import type { DistribuidorDto } from '../../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import distribuidorService from '../../../services/Distribucion/distribuidorService'; // Para cargar distribuidores
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
interface PorcPagoFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreatePorcPagoDto | UpdatePorcPagoDto, idPorcentaje?: number) => Promise<void>;
|
||||
idPublicacion: number;
|
||||
initialData?: PorcPagoDto | null; // Para editar
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const PorcPagoFormModal: React.FC<PorcPagoFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
idPublicacion,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
|
||||
const [vigenciaD, setVigenciaD] = useState(''); // "yyyy-MM-dd"
|
||||
const [vigenciaH, setVigenciaH] = useState(''); // "yyyy-MM-dd"
|
||||
const [porcentaje, setPorcentaje] = useState<string>('');
|
||||
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDistribuidores, setLoadingDistribuidores] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDistribuidores = async () => {
|
||||
setLoadingDistribuidores(true);
|
||||
try {
|
||||
const data = await distribuidorService.getAllDistribuidores();
|
||||
setDistribuidores(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar distribuidores", error);
|
||||
setLocalErrors(prev => ({...prev, distribuidores: 'Error al cargar distribuidores.'}));
|
||||
} finally {
|
||||
setLoadingDistribuidores(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchDistribuidores();
|
||||
setIdDistribuidor(initialData?.idDistribuidor || '');
|
||||
setVigenciaD(initialData?.vigenciaD || '');
|
||||
setVigenciaH(initialData?.vigenciaH || '');
|
||||
setPorcentaje(initialData?.porcentaje?.toString() || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!idDistribuidor) errors.idDistribuidor = 'Debe seleccionar un distribuidor.';
|
||||
if (!isEditing && !vigenciaD.trim()) {
|
||||
errors.vigenciaD = 'La Vigencia Desde es obligatoria.';
|
||||
} else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) {
|
||||
errors.vigenciaD = 'Formato de Vigencia Desde inválido (YYYY-MM-DD).';
|
||||
}
|
||||
if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) {
|
||||
errors.vigenciaH = 'Formato de Vigencia Hasta inválido (YYYY-MM-DD).';
|
||||
} else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) {
|
||||
errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.';
|
||||
}
|
||||
if (!porcentaje.trim()) errors.porcentaje = 'El porcentaje es obligatorio.';
|
||||
else {
|
||||
const porcNum = parseFloat(porcentaje);
|
||||
if (isNaN(porcNum) || porcNum < 0 || porcNum > 100) {
|
||||
errors.porcentaje = 'El porcentaje debe ser un número entre 0 y 100.';
|
||||
}
|
||||
}
|
||||
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 porcentajeNum = parseFloat(porcentaje);
|
||||
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdatePorcPagoDto = {
|
||||
porcentaje: porcentajeNum,
|
||||
vigenciaH: vigenciaH.trim() ? vigenciaH : null,
|
||||
};
|
||||
await onSubmit(dataToSubmit, initialData.idPorcentaje);
|
||||
} else {
|
||||
const dataToSubmit: CreatePorcPagoDto = {
|
||||
idPublicacion,
|
||||
idDistribuidor: Number(idDistribuidor),
|
||||
vigenciaD,
|
||||
porcentaje: porcentajeNum,
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de PorcPagoFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Porcentaje de Pago' : 'Agregar Nuevo Porcentaje de Pago'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idDistribuidor}>
|
||||
<InputLabel id="distribuidor-porc-select-label" required>Distribuidor</InputLabel>
|
||||
<Select labelId="distribuidor-porc-select-label" label="Distribuidor" value={idDistribuidor}
|
||||
onChange={(e) => {setIdDistribuidor(e.target.value as number); handleInputChange('idDistribuidor');}}
|
||||
disabled={loading || loadingDistribuidores || isEditing} // Distribuidor no se edita
|
||||
>
|
||||
<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">{localErrors.idDistribuidor}</Typography>}
|
||||
</FormControl>
|
||||
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing}
|
||||
onChange={(e) => {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}}
|
||||
margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
|
||||
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
|
||||
/>
|
||||
{isEditing && (
|
||||
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH}
|
||||
onChange={(e) => {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}}
|
||||
margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''}
|
||||
disabled={loading} InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
)}
|
||||
<TextField label="Porcentaje" type="number" value={porcentaje} required
|
||||
onChange={(e) => {setPorcentaje(e.target.value); handleInputChange('porcentaje');}}
|
||||
margin="dense" fullWidth error={!!localErrors.porcentaje} helperText={localErrors.porcentaje || ''}
|
||||
disabled={loading}
|
||||
InputProps={{ endAdornment: <InputAdornment position="end">%</InputAdornment> }}
|
||||
inputProps={{ step: "0.01", lang:"es-AR" }}
|
||||
/>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.distribuidores && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.distribuidores}</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 || loadingDistribuidores}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Porcentaje')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PorcPagoFormModal;
|
||||
@@ -0,0 +1,130 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControlLabel, Checkbox
|
||||
} from '@mui/material';
|
||||
import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto';
|
||||
import type { CreatePubliSeccionDto } from '../../../models/dtos/Distribucion/CreatePubliSeccionDto';
|
||||
import type { UpdatePubliSeccionDto } from '../../../models/dtos/Distribucion/UpdatePubliSeccionDto';
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 450 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
interface PubliSeccionFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreatePubliSeccionDto | UpdatePubliSeccionDto, idSeccion?: number) => Promise<void>;
|
||||
idPublicacion: number; // Siempre necesario para la creación
|
||||
initialData?: PubliSeccionDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const PubliSeccionFormModal: React.FC<PubliSeccionFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
idPublicacion,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [nombre, setNombre] = useState('');
|
||||
const [estado, setEstado] = useState(true); // Default a activa
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrorNombre, setLocalErrorNombre] = useState<string | null>(null);
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setNombre(initialData?.nombre || '');
|
||||
setEstado(initialData ? initialData.estado : true);
|
||||
setLocalErrorNombre(null);
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
let isValid = true;
|
||||
if (!nombre.trim()) {
|
||||
setLocalErrorNombre('El nombre de la sección es obligatorio.');
|
||||
isValid = false;
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleInputChange = () => {
|
||||
if (localErrorNombre) setLocalErrorNombre(null);
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdatePubliSeccionDto = { nombre, estado };
|
||||
await onSubmit(dataToSubmit, initialData.idSeccion);
|
||||
} else {
|
||||
const dataToSubmit: CreatePubliSeccionDto = {
|
||||
idPublicacion, // Viene de props
|
||||
nombre,
|
||||
estado
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de PubliSeccionFormModal:", error);
|
||||
// El error de API se maneja en la página
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Sección' : 'Agregar Nueva Sección'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<TextField label="Nombre de la Sección" value={nombre} required
|
||||
onChange={(e) => {setNombre(e.target.value); handleInputChange();}}
|
||||
margin="dense" fullWidth error={!!localErrorNombre} helperText={localErrorNombre || ''}
|
||||
disabled={loading} autoFocus
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={estado} onChange={(e) => setEstado(e.target.checked)} disabled={loading}/>}
|
||||
label="Activa" sx={{mt:1}}
|
||||
/>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</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}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Sección')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PubliSeccionFormModal;
|
||||
@@ -0,0 +1,193 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, InputAdornment
|
||||
} from '@mui/material';
|
||||
import type { RecargoZonaDto } from '../../../models/dtos/Distribucion/RecargoZonaDto';
|
||||
import type { CreateRecargoZonaDto } from '../../../models/dtos/Distribucion/CreateRecargoZonaDto';
|
||||
import type { UpdateRecargoZonaDto } from '../../../models/dtos/Distribucion/UpdateRecargoZonaDto';
|
||||
import type { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto'; // Para el dropdown de zonas
|
||||
import zonaService from '../../../services/Distribucion/zonaService'; // Para cargar zonas
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
interface RecargoZonaFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateRecargoZonaDto | UpdateRecargoZonaDto, idRecargo?: number) => Promise<void>;
|
||||
idPublicacion: number;
|
||||
initialData?: RecargoZonaDto | null; // Para editar
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const RecargoZonaFormModal: React.FC<RecargoZonaFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
idPublicacion,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [idZona, setIdZona] = useState<number | string>('');
|
||||
const [vigenciaD, setVigenciaD] = useState(''); // "yyyy-MM-dd"
|
||||
const [vigenciaH, setVigenciaH] = useState(''); // "yyyy-MM-dd"
|
||||
const [valor, setValor] = useState<string>('');
|
||||
|
||||
const [zonas, setZonas] = useState<ZonaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingZonas, setLoadingZonas] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchZonas = async () => {
|
||||
setLoadingZonas(true);
|
||||
try {
|
||||
const data = await zonaService.getAllZonas(); // Asume que devuelve zonas activas
|
||||
setZonas(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar zonas", error);
|
||||
setLocalErrors(prev => ({...prev, zonas: 'Error al cargar zonas.'}));
|
||||
} finally {
|
||||
setLoadingZonas(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchZonas();
|
||||
setIdZona(initialData?.idZona || '');
|
||||
setVigenciaD(initialData?.vigenciaD || '');
|
||||
setVigenciaH(initialData?.vigenciaH || '');
|
||||
setValor(initialData?.valor?.toString() || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!idZona) errors.idZona = 'Debe seleccionar una zona.';
|
||||
if (!isEditing && !vigenciaD.trim()) {
|
||||
errors.vigenciaD = 'La Vigencia Desde es obligatoria.';
|
||||
} else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) {
|
||||
errors.vigenciaD = 'Formato de Vigencia Desde inválido (YYYY-MM-DD).';
|
||||
}
|
||||
if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) {
|
||||
errors.vigenciaH = 'Formato de Vigencia Hasta inválido (YYYY-MM-DD).';
|
||||
} else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) {
|
||||
errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.';
|
||||
}
|
||||
if (!valor.trim()) errors.valor = 'El valor es obligatorio.';
|
||||
else if (isNaN(parseFloat(valor)) || parseFloat(valor) < 0) {
|
||||
errors.valor = 'El valor debe ser un número positivo.';
|
||||
}
|
||||
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 valorNum = parseFloat(valor);
|
||||
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdateRecargoZonaDto = {
|
||||
valor: valorNum,
|
||||
vigenciaH: vigenciaH.trim() ? vigenciaH : null,
|
||||
};
|
||||
await onSubmit(dataToSubmit, initialData.idRecargo);
|
||||
} else {
|
||||
const dataToSubmit: CreateRecargoZonaDto = {
|
||||
idPublicacion,
|
||||
idZona: Number(idZona),
|
||||
vigenciaD,
|
||||
valor: valorNum,
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de RecargoZonaFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Recargo por Zona' : 'Agregar Nuevo Recargo por Zona'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idZona}>
|
||||
<InputLabel id="zona-recargo-select-label" required>Zona</InputLabel>
|
||||
<Select labelId="zona-recargo-select-label" label="Zona" value={idZona}
|
||||
onChange={(e) => {setIdZona(e.target.value as number); handleInputChange('idZona');}}
|
||||
disabled={loading || loadingZonas || isEditing} // Zona no se edita
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione una zona</em></MenuItem>
|
||||
{zonas.map((z) => (<MenuItem key={z.idZona} value={z.idZona}>{z.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idZona && <Typography color="error" variant="caption">{localErrors.idZona}</Typography>}
|
||||
</FormControl>
|
||||
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing}
|
||||
onChange={(e) => {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}}
|
||||
margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
|
||||
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
|
||||
/>
|
||||
{isEditing && (
|
||||
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH}
|
||||
onChange={(e) => {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}}
|
||||
margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''}
|
||||
disabled={loading} InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
)}
|
||||
<TextField label="Valor Recargo" type="number" value={valor} required
|
||||
onChange={(e) => {setValor(e.target.value); handleInputChange('valor');}}
|
||||
margin="dense" fullWidth error={!!localErrors.valor} helperText={localErrors.valor || ''}
|
||||
disabled={loading}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }}
|
||||
inputProps={{ step: "0.01", lang:"es-AR" }}
|
||||
/>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.zonas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.zonas}</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 || loadingZonas}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Recargo')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecargoZonaFormModal;
|
||||
@@ -0,0 +1,199 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import type { SalidaOtroDestinoDto } from '../../../models/dtos/Distribucion/SalidaOtroDestinoDto';
|
||||
import type { CreateSalidaOtroDestinoDto } from '../../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto';
|
||||
import type { UpdateSalidaOtroDestinoDto } from '../../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto';
|
||||
import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { OtroDestinoDto } from '../../../models/dtos/Distribucion/OtroDestinoDto';
|
||||
import publicacionService from '../../../services/Distribucion/publicacionService';
|
||||
import otroDestinoService from '../../../services/Distribucion/otroDestinoService';
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
interface SalidaOtroDestinoFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateSalidaOtroDestinoDto | UpdateSalidaOtroDestinoDto, idParte?: number) => Promise<void>;
|
||||
initialData?: SalidaOtroDestinoDto | null; // Para editar
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const SalidaOtroDestinoFormModal: React.FC<SalidaOtroDestinoFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
|
||||
const [idDestino, setIdDestino] = useState<number | string>('');
|
||||
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [cantidad, setCantidad] = useState<string>('');
|
||||
const [observacion, setObservacion] = useState('');
|
||||
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDropdownData = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [pubsData, destinosData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true), // Solo habilitadas
|
||||
otroDestinoService.getAllOtrosDestinos()
|
||||
]);
|
||||
setPublicaciones(pubsData);
|
||||
setOtrosDestinos(destinosData);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar datos para dropdowns", error);
|
||||
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'}));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchDropdownData();
|
||||
setIdPublicacion(initialData?.idPublicacion || '');
|
||||
setIdDestino(initialData?.idDestino || '');
|
||||
setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]);
|
||||
setCantidad(initialData?.cantidad?.toString() || '');
|
||||
setObservacion(initialData?.observacion || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.';
|
||||
if (!idDestino) errors.idDestino = 'Seleccione un destino.';
|
||||
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 (!cantidad.trim() || isNaN(parseInt(cantidad)) || parseInt(cantidad) <= 0) {
|
||||
errors.cantidad = 'La cantidad debe ser un número positivo.';
|
||||
}
|
||||
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 {
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdateSalidaOtroDestinoDto = {
|
||||
cantidad: parseInt(cantidad, 10),
|
||||
observacion: observacion || undefined,
|
||||
};
|
||||
await onSubmit(dataToSubmit, initialData.idParte);
|
||||
} else {
|
||||
const dataToSubmit: CreateSalidaOtroDestinoDto = {
|
||||
idPublicacion: Number(idPublicacion),
|
||||
idDestino: Number(idDestino),
|
||||
fecha,
|
||||
cantidad: parseInt(cantidad, 10),
|
||||
observacion: observacion || undefined,
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de SalidaOtroDestinoFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Salida a Otro Destino' : 'Registrar Salida a Otro Destino'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
|
||||
<InputLabel id="publicacion-sod-select-label">Publicación</InputLabel>
|
||||
<Select labelId="publicacion-sod-select-label" label="Publicación" value={idPublicacion}
|
||||
onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}}
|
||||
disabled={loading || loadingDropdowns || isEditing} // No se edita publicación
|
||||
>
|
||||
<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">{localErrors.idPublicacion}</Typography>}
|
||||
</FormControl>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idDestino} required>
|
||||
<InputLabel id="destino-sod-select-label">Otro Destino</InputLabel>
|
||||
<Select labelId="destino-sod-select-label" label="Otro Destino" value={idDestino}
|
||||
onChange={(e) => {setIdDestino(e.target.value as number); handleInputChange('idDestino');}}
|
||||
disabled={loading || loadingDropdowns || isEditing} // No se edita destino
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
||||
{otrosDestinos.map((d) => (<MenuItem key={d.idDestino} value={d.idDestino}>{d.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idDestino && <Typography color="error" variant="caption">{localErrors.idDestino}</Typography>}
|
||||
</FormControl>
|
||||
<TextField label="Fecha" type="date" value={fecha} required
|
||||
onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}}
|
||||
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
|
||||
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
|
||||
/>
|
||||
<TextField label="Cantidad" type="number" value={cantidad} required
|
||||
onChange={(e) => {setCantidad(e.target.value); handleInputChange('cantidad');}}
|
||||
margin="dense" fullWidth error={!!localErrors.cantidad} helperText={localErrors.cantidad || ''}
|
||||
disabled={loading} inputProps={{min:1}}
|
||||
/>
|
||||
<TextField label="Observación (Opcional)" value={observacion}
|
||||
onChange={(e) => setObservacion(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}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Salida')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalidaOtroDestinoFormModal;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
334
Frontend/src/components/Modals/Impresion/TiradaFormModal.tsx
Normal file
334
Frontend/src/components/Modals/Impresion/TiradaFormModal.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user