Files
GestionIntegralWeb/Frontend/src/components/Modals/Distribucion/RecargoZonaFormModal.tsx

193 lines
8.1 KiB
TypeScript
Raw Normal View History

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.
2025-05-21 14:58:52 -03:00
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;