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