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

Backend API:
- Recargos por Zona (`dist_RecargoZona`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/recargos`.
  - Lógica de negocio para vigencias (cierre/reapertura de períodos).
  - Auditoría en `dist_RecargoZona_H`.
- Porcentajes de Pago Distribuidores (`dist_PorcPago`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajespago`.
  - Lógica de negocio para vigencias.
  - Auditoría en `dist_PorcPago_H`.
- Porcentajes/Montos Pago Canillitas (`dist_PorcMonPagoCanilla`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajesmoncanilla`.
  - Lógica de negocio para vigencias.
  - Auditoría en `dist_PorcMonPagoCanilla_H`.
- Secciones de Publicación (`dist_dtPubliSecciones`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/secciones`.
  - Auditoría en `dist_dtPubliSecciones_H`.
- Entradas/Salidas Distribuidores (`dist_EntradasSalidas`):
  - Implementado backend (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Lógica para determinar precios/recargos/porcentajes aplicables.
  - Cálculo de monto y afectación de saldos de distribuidores en `cue_Saldos`.
  - Auditoría en `dist_EntradasSalidas_H`.
- Correcciones de Mapeo Dapper:
  - Aplicados alias explícitos en repositorios de RecargoZona, PorcPago, PorcMonCanilla, PubliSeccion,
    Canilla, Distribuidor y Precio para asegurar mapeo correcto de IDs y columnas.

Frontend React:
- Recargos por Zona:
  - `recargoZonaService.ts`.
  - `RecargoZonaFormModal.tsx` para crear/editar períodos de recargos.
  - `GestionarRecargosPublicacionPage.tsx` para listar y gestionar recargos por publicación.
- Porcentajes de Pago Distribuidores:
  - `porcPagoService.ts`.
  - `PorcPagoFormModal.tsx`.
  - `GestionarPorcentajesPagoPage.tsx`.
- Porcentajes/Montos Pago Canillitas:
  - `porcMonCanillaService.ts`.
  - `PorcMonCanillaFormModal.tsx`.
  - `GestionarPorcMonCanillaPage.tsx`.
- Secciones de Publicación:
  - `publiSeccionService.ts`.
  - `PubliSeccionFormModal.tsx`.
  - `GestionarSeccionesPublicacionPage.tsx`.
- Navegación:
  - Actualizadas rutas y menús para acceder a la gestión de recargos, porcentajes (dist. y canillita) y secciones desde la vista de una publicación.
- Layout:
  - Uso consistente de `Box` con Flexbox en lugar de `Grid` en nuevos modales y páginas para evitar errores de tipo.
This commit is contained in:
2025-05-21 14:58:52 -03:00
parent b6ba52f074
commit e7e185a9cb
140 changed files with 10465 additions and 394 deletions

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;