feat: Implementación CRUD Canillitas, Distribuidores y Precios de Publicación
Backend API:
- Canillitas (`dist_dtCanillas`):
  - Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Lógica para manejo de `Accionista`, `Baja`, `FechaBaja`.
  - Auditoría en `dist_dtCanillas_H`.
  - Validación de legajo único y lógica de empresa vs accionista.
- Distribuidores (`dist_dtDistribuidores`):
  - Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Auditoría en `dist_dtDistribuidores_H`.
  - Creación de saldos iniciales para el nuevo distribuidor en todas las empresas.
  - Verificación de NroDoc único y Nombre opcionalmente único.
- Precios de Publicación (`dist_Precios`):
  - Implementado CRUD básico (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/precios`.
  - Lógica de negocio para cerrar período de precio anterior al crear uno nuevo.
  - Lógica de negocio para reabrir período de precio anterior al eliminar el último.
  - Auditoría en `dist_Precios_H`.
- Auditoría en Eliminación de Publicaciones:
  - Extendido `PublicacionService.EliminarAsync` para eliminar en cascada registros de precios, recargos, porcentajes de pago (distribuidores y canillitas) y secciones de publicación.
  - Repositorios correspondientes (`PrecioRepository`, `RecargoZonaRepository`, `PorcPagoRepository`, `PorcMonCanillaRepository`, `PubliSeccionRepository`) actualizados con métodos `DeleteByPublicacionIdAsync` que registran en sus respectivas tablas `_H` (si existen y se implementó la lógica).
  - Asegurada la correcta propagación del `idUsuario` para la auditoría en cascada.
- Correcciones de Nulabilidad:
  - Ajustados los métodos `MapToDto` y su uso en `CanillaService` y `PublicacionService` para manejar correctamente tipos anulables.
Frontend React:
- Canillitas:
  - `canillaService.ts`.
  - `CanillaFormModal.tsx` con selectores para Zona y Empresa, y lógica de Accionista.
  - `GestionarCanillitasPage.tsx` con filtros, paginación, y acciones (editar, toggle baja).
- Distribuidores:
  - `distribuidorService.ts`.
  - `DistribuidorFormModal.tsx` con múltiples campos y selector de Zona.
  - `GestionarDistribuidoresPage.tsx` con filtros, paginación, y acciones (editar, eliminar).
- Precios de Publicación:
  - `precioService.ts`.
  - `PrecioFormModal.tsx` para crear/editar períodos de precios (VigenciaD, VigenciaH opcional, precios por día).
  - `GestionarPreciosPublicacionPage.tsx` accesible desde la gestión de publicaciones, para listar y gestionar los períodos de precios de una publicación específica.
- Layout:
  - Reemplazado el uso de `Grid` por `Box` con Flexbox en `CanillaFormModal`, `GestionarCanillitasPage` (filtros), `DistribuidorFormModal` y `PrecioFormModal` para resolver problemas de tipos y mejorar la consistencia del layout de formularios.
- Navegación:
  - Actualizadas las rutas y pestañas para los nuevos módulos y sub-módulos.
			
			
This commit is contained in:
		
							
								
								
									
										239
									
								
								Frontend/src/components/Modals/Distribucion/CanillaFormModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								Frontend/src/components/Modals/Distribucion/CanillaFormModal.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,239 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Modal, Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
|     FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox | ||||
| } from '@mui/material'; | ||||
| import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; | ||||
| import type { CreateCanillaDto } from '../../../models/dtos/Distribucion/CreateCanillaDto'; | ||||
| import type { UpdateCanillaDto } from '../../../models/dtos/Distribucion/UpdateCanillaDto'; | ||||
| import type { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto'; // Para el dropdown de zonas | ||||
| import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto'; // Para el dropdown de empresas | ||||
| import zonaService from '../../../services/Distribucion/zonaService'; | ||||
| import empresaService from '../../../services/Distribucion/empresaService'; | ||||
|  | ||||
| const modalStyle = { | ||||
|     position: 'absolute' as 'absolute', | ||||
|     top: '50%', | ||||
|     left: '50%', | ||||
|     transform: 'translate(-50%, -50%)', | ||||
|     width: { xs: '90%', sm: 600 }, // Un poco más ancho | ||||
|     bgcolor: 'background.paper', | ||||
|     border: '2px solid #000', | ||||
|     boxShadow: 24, | ||||
|     p: 4, | ||||
|     maxHeight: '90vh', | ||||
|     overflowY: 'auto' | ||||
| }; | ||||
|  | ||||
| interface CanillaFormModalProps { | ||||
|   open: boolean; | ||||
|   onClose: () => void; | ||||
|   onSubmit: (data: CreateCanillaDto | UpdateCanillaDto, id?: number) => Promise<void>; | ||||
|   initialData?: CanillaDto | null; | ||||
|   errorMessage?: string | null; | ||||
|   clearErrorMessage: () => void; | ||||
| } | ||||
|  | ||||
| const CanillaFormModal: React.FC<CanillaFormModalProps> = ({ | ||||
|   open, | ||||
|   onClose, | ||||
|   onSubmit, | ||||
|   initialData, | ||||
|   errorMessage, | ||||
|   clearErrorMessage | ||||
| }) => { | ||||
|   const [legajo, setLegajo] = useState<string>(''); // Manejar como string para el TextField | ||||
|   const [nomApe, setNomApe] = useState(''); | ||||
|   const [parada, setParada] = useState(''); | ||||
|   const [idZona, setIdZona] = useState<number | string>(''); | ||||
|   const [accionista, setAccionista] = useState(false); | ||||
|   const [obs, setObs] = useState(''); | ||||
|   const [empresa, setEmpresa] = useState<number | string>(0); // 0 para N/A (accionista) | ||||
|  | ||||
|   const [zonas, setZonas] = useState<ZonaDto[]>([]); | ||||
|   const [empresas, setEmpresas] = useState<EmpresaDto[]>([]); | ||||
|   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 [zonasData, empresasData] = await Promise.all([ | ||||
|                 zonaService.getAllZonas(), // Asume que este servicio devuelve zonas activas | ||||
|                 empresaService.getAllEmpresas() | ||||
|             ]); | ||||
|             setZonas(zonasData); | ||||
|             setEmpresas(empresasData); | ||||
|         } catch (error) { | ||||
|             console.error("Error al cargar datos para dropdowns", error); | ||||
|             setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar zonas/empresas.'})); | ||||
|         } finally { | ||||
|             setLoadingDropdowns(false); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     if (open) { | ||||
|         fetchDropdownData(); | ||||
|         setLegajo(initialData?.legajo?.toString() || ''); | ||||
|         setNomApe(initialData?.nomApe || ''); | ||||
|         setParada(initialData?.parada || ''); | ||||
|         setIdZona(initialData?.idZona || ''); | ||||
|         setAccionista(initialData ? initialData.accionista : false); | ||||
|         setObs(initialData?.obs || ''); | ||||
|         setEmpresa(initialData ? initialData.empresa : 0); // Si es accionista, empresa es 0 | ||||
|         setLocalErrors({}); | ||||
|         clearErrorMessage(); | ||||
|     } | ||||
|   }, [open, initialData, clearErrorMessage]); | ||||
|  | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     const errors: { [key: string]: string | null } = {}; | ||||
|     if (!nomApe.trim()) errors.nomApe = 'Nombre y Apellido son obligatorios.'; | ||||
|     if (!idZona) errors.idZona = 'Debe seleccionar una zona.'; | ||||
|  | ||||
|     const legajoNum = legajo ? parseInt(legajo, 10) : null; | ||||
|     if (legajo.trim() && (isNaN(legajoNum!) || legajoNum! < 0)) { | ||||
|         errors.legajo = 'Legajo debe ser un número positivo o vacío.'; | ||||
|     } | ||||
|  | ||||
|     // Lógica de empresa y accionista | ||||
|     if (accionista && empresa !== 0 && empresa !== '') { | ||||
|         errors.empresa = 'Si es Accionista, la Empresa debe ser N/A (0).'; | ||||
|     } | ||||
|     if (!accionista && (empresa === 0 || empresa === '')) { | ||||
|         errors.empresa = 'Si no es Accionista, debe seleccionar una Empresa.'; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     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 legajoParsed = legajo.trim() ? parseInt(legajo, 10) : null; | ||||
|  | ||||
|       const dataToSubmit = { | ||||
|           legajo: legajoParsed, | ||||
|           nomApe, | ||||
|           parada: parada || undefined, | ||||
|           idZona: Number(idZona), | ||||
|           accionista, | ||||
|           obs: obs || undefined, | ||||
|           empresa: Number(empresa) | ||||
|       }; | ||||
|  | ||||
|       if (isEditing && initialData) { | ||||
|         await onSubmit(dataToSubmit as UpdateCanillaDto, initialData.idCanilla); | ||||
|       } else { | ||||
|         await onSubmit(dataToSubmit as CreateCanillaDto); | ||||
|       } | ||||
|       onClose(); | ||||
|     } catch (error: any) { | ||||
|       console.error("Error en submit de CanillaFormModal:", error); | ||||
|     } finally { | ||||
|        setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Modal open={open} onClose={onClose}> | ||||
|       <Box sx={modalStyle}> | ||||
|         <Typography variant="h6" component="h2" gutterBottom> | ||||
|           {isEditing ? 'Editar Canillita' : 'Agregar Nuevo Canillita'} | ||||
|         </Typography> | ||||
|         <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}> | ||||
|             {/* SECCIÓN DE CAMPOS CON BOX Y FLEXBOX */} | ||||
|             <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | ||||
|                 <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start' }}> | ||||
|                     <TextField label="Legajo" value={legajo} type="number" | ||||
|                         onChange={(e) => {setLegajo(e.target.value); handleInputChange('legajo');}} | ||||
|                         margin="dense" fullWidth error={!!localErrors.legajo} helperText={localErrors.legajo || ''} | ||||
|                         disabled={loading} | ||||
|                         sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} | ||||
|                     /> | ||||
|                     <TextField label="Nombre y Apellido" value={nomApe} required | ||||
|                         onChange={(e) => {setNomApe(e.target.value); handleInputChange('nomApe');}} | ||||
|                         margin="dense" fullWidth error={!!localErrors.nomApe} helperText={localErrors.nomApe || ''} | ||||
|                         disabled={loading} autoFocus={!isEditing} | ||||
|                         sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} | ||||
|                     /> | ||||
|                 </Box> | ||||
|                 <TextField label="Parada (Dirección)" value={parada} | ||||
|                     onChange={(e) => setParada(e.target.value)} | ||||
|                     margin="dense" fullWidth multiline rows={2} disabled={loading} | ||||
|                 /> | ||||
|                 <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start', mt: 1 }}> | ||||
|                     <FormControl fullWidth margin="dense" error={!!localErrors.idZona} sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}> | ||||
|                         <InputLabel id="zona-select-label" required>Zona</InputLabel> | ||||
|                         <Select labelId="zona-select-label" label="Zona" value={idZona} | ||||
|                             onChange={(e) => {setIdZona(e.target.value as number); handleInputChange('idZona');}} | ||||
|                             disabled={loading || loadingDropdowns} | ||||
|                         > | ||||
|                             <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> | ||||
|                     <FormControl fullWidth margin="dense" error={!!localErrors.empresa} sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}> | ||||
|                         <InputLabel id="empresa-select-label">Empresa</InputLabel> | ||||
|                         <Select labelId="empresa-select-label" label="Empresa" value={empresa} | ||||
|                             onChange={(e) => {setEmpresa(e.target.value as number); handleInputChange('empresa');}} | ||||
|                             disabled={loading || loadingDropdowns || accionista} | ||||
|                         > | ||||
|                             <MenuItem value={0}><em>N/A (Accionista)</em></MenuItem> | ||||
|                             {empresas.map((e) => (<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>))} | ||||
|                         </Select> | ||||
|                         {localErrors.empresa && <Typography color="error" variant="caption">{localErrors.empresa}</Typography>} | ||||
|                     </FormControl> | ||||
|                 </Box> | ||||
|                 <TextField label="Observaciones" value={obs} | ||||
|                     onChange={(e) => setObs(e.target.value)} | ||||
|                     margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{mt:1}} | ||||
|                 /> | ||||
|                 <Box sx={{mt:1}}> | ||||
|                     <FormControlLabel | ||||
|                         control={<Checkbox checked={accionista} onChange={(e) => { | ||||
|                             setAccionista(e.target.checked); | ||||
|                             if (e.target.checked) setEmpresa(0); | ||||
|                             handleInputChange('accionista'); | ||||
|                             handleInputChange('empresa'); | ||||
|                         }} disabled={loading}/>} | ||||
|                         label="Es Accionista" | ||||
|                     /> | ||||
|                 </Box> | ||||
|             </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' : 'Crear Canillita')} | ||||
|             </Button> | ||||
|           </Box> | ||||
|         </Box> | ||||
|       </Box> | ||||
|     </Modal> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default CanillaFormModal; | ||||
		Reference in New Issue
	
	Block a user