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.
		
	
		
			
				
	
	
		
			180 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			180 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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 { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto';
 | |
| import type { CreatePublicacionDto } from '../../../models/dtos/Distribucion/CreatePublicacionDto';
 | |
| import type { UpdatePublicacionDto } from '../../../models/dtos/Distribucion/UpdatePublicacionDto';
 | |
| import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto';
 | |
| import empresaService from '../../../services/Distribucion/empresaService'; // Para cargar empresas
 | |
| 
 | |
| 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 PublicacionFormModalProps {
 | |
|   open: boolean;
 | |
|   onClose: () => void;
 | |
|   onSubmit: (data: CreatePublicacionDto | UpdatePublicacionDto, id?: number) => Promise<void>;
 | |
|   initialData?: PublicacionDto | null;
 | |
|   errorMessage?: string | null;
 | |
|   clearErrorMessage: () => void;
 | |
| }
 | |
| 
 | |
| const PublicacionFormModal: React.FC<PublicacionFormModalProps> = ({
 | |
|   open,
 | |
|   onClose,
 | |
|   onSubmit,
 | |
|   initialData,
 | |
|   errorMessage,
 | |
|   clearErrorMessage
 | |
| }) => {
 | |
|   const [nombre, setNombre] = useState('');
 | |
|   const [observacion, setObservacion] = useState('');
 | |
|   const [idEmpresa, setIdEmpresa] = useState<number | string>('');
 | |
|   const [ctrlDevoluciones, setCtrlDevoluciones] = useState(false);
 | |
|   const [habilitada, setHabilitada] = useState(true);
 | |
| 
 | |
|   const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
 | |
|   const [loading, setLoading] = useState(false);
 | |
|   const [loadingEmpresas, setLoadingEmpresas] = useState(false);
 | |
|   const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
 | |
| 
 | |
|   const isEditing = Boolean(initialData);
 | |
| 
 | |
|   useEffect(() => {
 | |
|     const fetchEmpresas = async () => {
 | |
|         setLoadingEmpresas(true);
 | |
|         try {
 | |
|             const data = await empresaService.getAllEmpresas();
 | |
|             setEmpresas(data);
 | |
|         } catch (error) {
 | |
|             console.error("Error al cargar empresas", error);
 | |
|             setLocalErrors(prev => ({...prev, empresas: 'Error al cargar empresas.'}));
 | |
|         } finally {
 | |
|             setLoadingEmpresas(false);
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     if (open) {
 | |
|         fetchEmpresas();
 | |
|         setNombre(initialData?.nombre || '');
 | |
|         setObservacion(initialData?.observacion || '');
 | |
|         setIdEmpresa(initialData?.idEmpresa || '');
 | |
|         setCtrlDevoluciones(initialData ? initialData.ctrlDevoluciones : false);
 | |
|         setHabilitada(initialData ? initialData.habilitada : true);
 | |
|         setLocalErrors({});
 | |
|         clearErrorMessage();
 | |
|     }
 | |
|   }, [open, initialData, clearErrorMessage]);
 | |
| 
 | |
|   const validate = (): boolean => {
 | |
|     const errors: { [key: string]: string | null } = {};
 | |
|     if (!nombre.trim()) errors.nombre = 'El nombre es obligatorio.';
 | |
|     if (!idEmpresa) errors.idEmpresa = '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 dataToSubmit = {
 | |
|           nombre,
 | |
|           observacion: observacion || undefined,
 | |
|           idEmpresa: Number(idEmpresa),
 | |
|           ctrlDevoluciones,
 | |
|           habilitada
 | |
|       };
 | |
| 
 | |
|       if (isEditing && initialData) {
 | |
|         await onSubmit(dataToSubmit as UpdatePublicacionDto, initialData.idPublicacion);
 | |
|       } else {
 | |
|         await onSubmit(dataToSubmit as CreatePublicacionDto);
 | |
|       }
 | |
|       onClose();
 | |
|     } catch (error: any) {
 | |
|       console.error("Error en submit de PublicacionFormModal:", error);
 | |
|     } finally {
 | |
|        setLoading(false);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return (
 | |
|     <Modal open={open} onClose={onClose}>
 | |
|       <Box sx={modalStyle}>
 | |
|         <Typography variant="h6" component="h2" gutterBottom>
 | |
|           {isEditing ? 'Editar Publicación' : 'Agregar Nueva Publicación'}
 | |
|         </Typography>
 | |
|         <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
 | |
|              <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
 | |
|                 <TextField label="Nombre Publicación" value={nombre} required
 | |
|                     onChange={(e) => {setNombre(e.target.value); handleInputChange('nombre');}}
 | |
|                     margin="dense" fullWidth error={!!localErrors.nombre} helperText={localErrors.nombre || ''}
 | |
|                     disabled={loading} autoFocus={!isEditing}
 | |
|                 />
 | |
|                 <FormControl fullWidth margin="dense" error={!!localErrors.idEmpresa}>
 | |
|                     <InputLabel id="empresa-pub-select-label" required>Empresa</InputLabel>
 | |
|                     <Select labelId="empresa-pub-select-label" label="Empresa" value={idEmpresa}
 | |
|                         onChange={(e) => {setIdEmpresa(e.target.value as number); handleInputChange('idEmpresa');}}
 | |
|                         disabled={loading || loadingEmpresas}
 | |
|                     >
 | |
|                         <MenuItem value="" disabled><em>Seleccione una empresa</em></MenuItem>
 | |
|                         {empresas.map((e) => (<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>))}
 | |
|                     </Select>
 | |
|                     {localErrors.idEmpresa && <Typography color="error" variant="caption">{localErrors.idEmpresa}</Typography>}
 | |
|                 </FormControl>
 | |
|                 <TextField label="Observación (Opcional)" value={observacion}
 | |
|                     onChange={(e) => setObservacion(e.target.value)}
 | |
|                     margin="dense" fullWidth multiline rows={3} disabled={loading}
 | |
|                 />
 | |
|                 <Box sx={{ display: 'flex', justifyContent: 'space-around', mt: 1, flexWrap: 'wrap' }}>
 | |
|                     <FormControlLabel
 | |
|                         control={<Checkbox checked={ctrlDevoluciones} onChange={(e) => setCtrlDevoluciones(e.target.checked)} disabled={loading}/>}
 | |
|                         label="Controla Devoluciones"
 | |
|                     />
 | |
|                     <FormControlLabel
 | |
|                         control={<Checkbox checked={habilitada} onChange={(e) => setHabilitada(e.target.checked)} disabled={loading}/>}
 | |
|                         label="Habilitada"
 | |
|                     />
 | |
|                 </Box>
 | |
|             </Box>
 | |
| 
 | |
|           {errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
 | |
|           {localErrors.empresas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.empresas}</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 || loadingEmpresas}>
 | |
|               {loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Crear Publicación')}
 | |
|             </Button>
 | |
|           </Box>
 | |
|         </Box>
 | |
|       </Box>
 | |
|     </Modal>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export default PublicacionFormModal; |