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.
		
	
		
			
				
	
	
		
			193 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			193 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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; |