255 lines
13 KiB
TypeScript
255 lines
13 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 { 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;
|