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.
206 lines
10 KiB
TypeScript
206 lines
10 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
|
FormControl, InputLabel, Select, MenuItem
|
|
} from '@mui/material';
|
|
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
|
import type { 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; |