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:
2025-05-20 12:38:55 -03:00
parent daf84d2708
commit b6ba52f074
228 changed files with 10745 additions and 178 deletions

View 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;