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,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; | ||||
		Reference in New Issue
	
	Block a user