Files
GestionIntegralWeb/Frontend/src/components/Modals/Usuarios/UsuarioFormModal.tsx

255 lines
13 KiB
TypeScript
Raw Normal View History

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.
2025-05-20 12:38:55 -03:00
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 { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto';
import type { CreateUsuarioRequestDto } from '../../../models/dtos/Usuarios/CreateUsuarioRequestDto';
import type { UpdateUsuarioRequestDto } from '../../../models/dtos/Usuarios/UpdateUsuarioRequestDto';
import type { PerfilDto } from '../../../models/dtos/Usuarios/PerfilDto'; // Para el dropdown de perfiles
import perfilService from '../../../services/Usuarios/perfilService'; // Para obtener la lista de perfiles
const modalStyle = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 500 }, // Responsive width
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh', // Para permitir scroll si el contenido es mucho
overflowY: 'auto' // Habilitar scroll vertical
};
interface UsuarioFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateUsuarioRequestDto | UpdateUsuarioRequestDto, id?: number) => Promise<void>;
initialData?: UsuarioDto | null;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const UsuarioFormModal: React.FC<UsuarioFormModalProps> = ({
open,
onClose,
onSubmit,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [user, setUser] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [nombre, setNombre] = useState('');
const [apellido, setApellido] = useState('');
const [idPerfil, setIdPerfil] = useState<number | string>(''); // Puede ser string vacío inicialmente
const [habilitada, setHabilitada] = useState(true);
const [supAdmin, setSupAdmin] = useState(false);
const [debeCambiarClave, setDebeCambiarClave] = useState(true);
const [verLog, setVerLog] = useState('1.0.0.0');
const [perfiles, setPerfiles] = useState<PerfilDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingPerfiles, setLoadingPerfiles] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const isEditing = Boolean(initialData);
useEffect(() => {
const fetchPerfiles = async () => {
setLoadingPerfiles(true);
try {
const data = await perfilService.getAllPerfiles();
setPerfiles(data);
} catch (error) {
console.error("Error al cargar perfiles", error);
setLocalErrors(prev => ({...prev, perfiles: 'Error al cargar perfiles.'}));
} finally {
setLoadingPerfiles(false);
}
};
if (open) {
fetchPerfiles();
setUser(initialData?.user || '');
// No pre-rellenar contraseña en edición
setPassword('');
setConfirmPassword('');
setNombre(initialData?.nombre || '');
setApellido(initialData?.apellido || '');
setIdPerfil(initialData?.idPerfil || '');
setHabilitada(initialData ? initialData.habilitada : true);
setSupAdmin(initialData ? initialData.supAdmin : false);
setDebeCambiarClave(initialData ? initialData.debeCambiarClave : !isEditing); // true para creación
setVerLog(initialData?.verLog || '1.0.0.0');
setLocalErrors({});
clearErrorMessage();
}
}, [open, initialData, clearErrorMessage, isEditing]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!user.trim()) errors.user = 'El nombre de usuario es obligatorio.';
else if (user.length < 3) errors.user = 'El usuario debe tener al menos 3 caracteres.';
if (!isEditing || (isEditing && password)) { // Validar contraseña solo si se está creando o si se ingresó algo en edición
if (!password) errors.password = 'La contraseña es obligatoria.';
else if (password.length < 6) errors.password = 'La contraseña debe tener al menos 6 caracteres.';
if (password !== confirmPassword) errors.confirmPassword = 'Las contraseñas no coinciden.';
if (user.trim().toLowerCase() === password.toLowerCase()) errors.password = 'La contraseña no puede ser igual al nombre de usuario.'
}
if (!nombre.trim()) errors.nombre = 'El nombre es obligatorio.';
if (!apellido.trim()) errors.apellido = 'El apellido es obligatorio.';
if (!idPerfil) errors.idPerfil = 'Debe seleccionar un perfil.';
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: UpdateUsuarioRequestDto = {
nombre, apellido, idPerfil: Number(idPerfil), habilitada, supAdmin, debeCambiarClave, verLog
};
await onSubmit(dataToSubmit, initialData.id);
// Si se ingresó una nueva contraseña, llamar a un endpoint separado para cambiarla
if (password) {
// Esto requeriría un endpoint adicional en el backend para que un admin cambie la clave de otro user
// o adaptar el flujo de `AuthService.ChangePasswordAsync` si es posible,
// o un nuevo endpoint en UsuarioController para setear clave.
// Por ahora, lo dejamos así, la clave se cambia por el propio usuario o por un reset del admin.
console.warn("El cambio de contraseña en edición de usuario no está implementado en este modal directamente. Usar la opción de 'Resetear Contraseña'.");
}
} else {
const dataToSubmit: CreateUsuarioRequestDto = {
user, password, nombre, apellido, idPerfil: Number(idPerfil), habilitada, supAdmin, debeCambiarClave, verLog
};
await onSubmit(dataToSubmit);
}
onClose();
} catch (error: any) {
console.error("Error en submit de UsuarioFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Usuario' : 'Agregar Nuevo Usuario'}
</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 }}> {/* Contenedor principal de campos */}
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}> {/* Fila 1 */}
<TextField label="Nombre de Usuario" fullWidth required value={user}
onChange={(e) => {setUser(e.target.value); handleInputChange('user');}} margin="dense"
error={!!localErrors.user} helperText={localErrors.user || ''}
disabled={loading || isEditing} autoFocus={!isEditing}
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} // 8px es la mitad del gap
/>
<FormControl fullWidth margin="dense" error={!!localErrors.idPerfil} sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}>
<InputLabel id="perfil-select-label" required>Perfil</InputLabel>
<Select
labelId="perfil-select-label"
label="Perfil"
value={idPerfil}
onChange={(e) => { setIdPerfil(e.target.value as number); handleInputChange('idPerfil');}}
disabled={loading || loadingPerfiles}
>
<MenuItem value="" disabled><em>Seleccione un perfil</em></MenuItem>
{perfiles.map((p) => ( <MenuItem key={p.id} value={p.id}>{p.nombrePerfil}</MenuItem> ))}
</Select>
{localErrors.idPerfil && <Typography color="error" variant="caption">{localErrors.idPerfil}</Typography>}
{loadingPerfiles && <CircularProgress size={20} sx={{position: 'absolute', right: 30, top: '50%', marginTop: '-10px'}}/>}
</FormControl>
</Box>
{!isEditing && (
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 1 }}> {/* Fila 2 (Contraseñas) */}
<TextField label="Contraseña" type="password" fullWidth required={!isEditing} value={password}
onChange={(e) => {setPassword(e.target.value); handleInputChange('password');}} margin="dense"
error={!!localErrors.password} helperText={localErrors.password || ''}
disabled={loading}
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
/>
<TextField label="Confirmar Contraseña" type="password" fullWidth required={!isEditing} value={confirmPassword}
onChange={(e) => {setConfirmPassword(e.target.value); handleInputChange('confirmPassword');}} margin="dense"
error={!!localErrors.confirmPassword} helperText={localErrors.confirmPassword || ''}
disabled={loading}
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
/>
</Box>
)}
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 1 }}> {/* Fila 3 (Nombre y Apellido) */}
<TextField label="Nombre" fullWidth required value={nombre}
onChange={(e) => {setNombre(e.target.value); handleInputChange('nombre');}} margin="dense"
error={!!localErrors.nombre} helperText={localErrors.nombre || ''}
disabled={loading}
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
/>
<TextField label="Apellido" fullWidth required value={apellido}
onChange={(e) => {setApellido(e.target.value); handleInputChange('apellido');}} margin="dense"
error={!!localErrors.apellido} helperText={localErrors.apellido || ''}
disabled={loading}
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 1, alignItems: 'center' }}> {/* Fila 4 (VerLog y Habilitado) */}
<TextField label="Versión Log" fullWidth value={verLog}
onChange={(e) => setVerLog(e.target.value)} margin="dense"
disabled={loading}
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
/>
<Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)', display: 'flex', justifyContent: 'flex-start', pl:1 /* o pt si es vertical */ }}>
<FormControlLabel control={<Checkbox checked={habilitada} onChange={(e) => setHabilitada(e.target.checked)} disabled={loading}/>} label="Habilitado" />
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 0.5 }}> {/* Fila 5 (Checkboxes) */}
<Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}>
<FormControlLabel control={<Checkbox checked={supAdmin} onChange={(e) => setSupAdmin(e.target.checked)} disabled={loading}/>} label="Super Administrador" />
</Box>
<Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}>
<FormControlLabel control={<Checkbox checked={debeCambiarClave} onChange={(e) => setDebeCambiarClave(e.target.checked)} disabled={loading}/>} label="Debe Cambiar Clave" />
</Box>
</Box>
</Box> {/* Fin contenedor principal de campos */}
{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' : 'Crear Usuario')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default UsuarioFormModal;