F3.1 WEB: editar perfil personal completo del Inspector
This commit is contained in:
@@ -1,25 +1,28 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { Link, useLocation, useParams } from 'react-router';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
||||
import { Icon } from '../components/Icon';
|
||||
import {
|
||||
getUser,
|
||||
listRoles,
|
||||
replaceUserRoles,
|
||||
resetUserPassword,
|
||||
updateUser,
|
||||
updateUserStatus,
|
||||
} from '../lib/api';
|
||||
import type { AdministrativeRole, AdministrativeUser } from '../lib/api';
|
||||
import type { AdministrativeRole } from '../lib/api';
|
||||
import {
|
||||
getUserProfile,
|
||||
updateUserProfile,
|
||||
} from '../lib/userProfileApi';
|
||||
import type { AdministrativeUserProfile } from '../lib/userProfileApi';
|
||||
import { formatDate, initials } from '../lib/format';
|
||||
|
||||
export function UserDetailPage() {
|
||||
const { id = '' } = useParams();
|
||||
const location = useLocation();
|
||||
const { user: currentUser, hasPermission } = useAuth();
|
||||
const [user, setUser] = useState<AdministrativeUser | null>(null);
|
||||
const [user, setUser] = useState<AdministrativeUserProfile | null>(null);
|
||||
const [roles, setRoles] = useState<AdministrativeRole[]>([]);
|
||||
const [roleIds, setRoleIds] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -33,7 +36,7 @@ export function UserDetailPage() {
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [loadedUser, loadedRoles] = await Promise.all([getUser(id), listRoles()]);
|
||||
const [loadedUser, loadedRoles] = await Promise.all([getUserProfile(id), listRoles()]);
|
||||
setUser(loadedUser);
|
||||
setRoles(loadedRoles);
|
||||
setRoleIds(loadedUser.roles.map((role) => role.id));
|
||||
@@ -46,27 +49,45 @@ export function UserDetailPage() {
|
||||
|
||||
useEffect(() => { void load(); }, [id]);
|
||||
|
||||
const inspectorRoleId = useMemo(() => roles.find((role) => role.code === 'inspector')?.id ?? null, [roles]);
|
||||
const inspectorSelected = inspectorRoleId ? roleIds.includes(inspectorRoleId) : false;
|
||||
|
||||
const saveProfile = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError(''); setSuccess(''); setSaving('profile');
|
||||
setError(''); setSuccess('');
|
||||
const data = new FormData(event.currentTarget);
|
||||
const email = String(data.get('email') ?? '').trim();
|
||||
if (inspectorSelected && !email) {
|
||||
setError('El email es obligatorio para un Inspector porque allí recibe la documentación de sus inspecciones.');
|
||||
return;
|
||||
}
|
||||
setSaving('profile');
|
||||
try {
|
||||
const updated = await updateUser(id, {
|
||||
const updated = await updateUserProfile(id, {
|
||||
firstName: String(data.get('firstName')),
|
||||
lastName: String(data.get('lastName')),
|
||||
username: String(data.get('username')),
|
||||
email: String(data.get('email') ?? '') || null,
|
||||
email: email || null,
|
||||
dni: String(data.get('dni') ?? '').trim() || null,
|
||||
phone: String(data.get('phone') ?? '').trim() || null,
|
||||
jobTitle: String(data.get('jobTitle') ?? '').trim() || null,
|
||||
employeeNumber: String(data.get('employeeNumber') ?? '').trim() || null,
|
||||
});
|
||||
setUser(updated); setSuccess('Datos del usuario actualizados');
|
||||
setUser(updated); setSuccess('Datos personales y de contacto actualizados');
|
||||
} catch (requestError) { setError(errorMessage(requestError)); }
|
||||
finally { setSaving(''); }
|
||||
};
|
||||
|
||||
const saveRoles = async () => {
|
||||
if (inspectorSelected && !user?.email) {
|
||||
setError('Antes de asignar el rol Inspector, cargá y guardá un email válido.');
|
||||
return;
|
||||
}
|
||||
setError(''); setSuccess(''); setSaving('roles');
|
||||
try {
|
||||
const updated = await replaceUserRoles(id, roleIds);
|
||||
setUser(updated); setSuccess('Roles actualizados correctamente');
|
||||
await replaceUserRoles(id, roleIds);
|
||||
const refreshed = await getUserProfile(id);
|
||||
setUser(refreshed); setSuccess('Roles actualizados correctamente');
|
||||
} catch (requestError) { setError(errorMessage(requestError)); }
|
||||
finally { setSaving(''); }
|
||||
};
|
||||
@@ -78,8 +99,9 @@ export function UserDetailPage() {
|
||||
if (!window.confirm(`¿Confirmás que querés ${verb} a ${user.username}?`)) return;
|
||||
setError(''); setSuccess(''); setSaving('status');
|
||||
try {
|
||||
const updated = await updateUserStatus(id, next);
|
||||
setUser(updated); setSuccess(`Usuario ${next === 'ACTIVE' ? 'activado' : 'desactivado'}`);
|
||||
await updateUserStatus(id, next);
|
||||
const refreshed = await getUserProfile(id);
|
||||
setUser(refreshed); setSuccess(`Usuario ${next === 'ACTIVE' ? 'activado' : 'desactivado'}`);
|
||||
} catch (requestError) { setError(errorMessage(requestError)); }
|
||||
finally { setSaving(''); }
|
||||
};
|
||||
@@ -91,8 +113,9 @@ export function UserDetailPage() {
|
||||
if (!window.confirm(`¿Restablecer la contraseña de ${user?.username ?? 'este usuario'}? Se cerrarán todas sus sesiones activas.`)) return;
|
||||
setError(''); setSuccess(''); setSaving('password');
|
||||
try {
|
||||
const updated = await resetUserPassword(id, resetPasswordValue, forcePasswordChange);
|
||||
setUser(updated);
|
||||
await resetUserPassword(id, resetPasswordValue, forcePasswordChange);
|
||||
const refreshed = await getUserProfile(id);
|
||||
setUser(refreshed);
|
||||
setResetPasswordValue(''); setResetPasswordConfirm('');
|
||||
setSuccess('Contraseña restablecida. Las sesiones anteriores fueron revocadas.');
|
||||
} catch (requestError) { setError(errorMessage(requestError)); }
|
||||
@@ -110,13 +133,23 @@ export function UserDetailPage() {
|
||||
|
||||
return <section>
|
||||
<div className="breadcrumb"><Link to="/admin/users">Usuarios</Link><span>/</span><strong>{user.username}</strong></div>
|
||||
<div className="page-heading user-heading"><div className="profile-title"><span className="profile-avatar">{initials(user.firstName, user.lastName)}</span><div><span className="eyebrow">DETALLE DE USUARIO</span><h1>{user.firstName} {user.lastName}</h1><p>@{user.username} · Creado {formatDate(user.createdAt)}</p></div></div>{canChangeStatus && <button className={`button ${user.status === 'ACTIVE' ? 'danger-outline' : 'success-outline'}`} disabled={saving === 'status' || currentUser?.id === user.id} onClick={changeStatus}>{user.status === 'ACTIVE' ? 'Desactivar acceso' : 'Activar acceso'}</button>}</div>
|
||||
<div className="page-heading user-heading"><div className="profile-title"><span className="profile-avatar">{initials(user.firstName, user.lastName)}</span><div><span className="eyebrow">DETALLE DE USUARIO</span><h1>{user.firstName} {user.lastName}</h1><p>@{user.username} · {user.jobTitle || 'Función sin informar'} · Creado {formatDate(user.createdAt)}</p></div></div>{canChangeStatus && <button className={`button ${user.status === 'ACTIVE' ? 'danger-outline' : 'success-outline'}`} disabled={saving === 'status' || currentUser?.id === user.id} onClick={changeStatus}>{user.status === 'ACTIVE' ? 'Desactivar acceso' : 'Activar acceso'}</button>}</div>
|
||||
{error && <Alert>{error}</Alert>}{success && <Alert type="success">{success}</Alert>}
|
||||
|
||||
<div className="detail-grid">
|
||||
<form className="panel form-panel" onSubmit={saveProfile}>
|
||||
<div className="panel-heading"><div><span className="eyebrow">CUENTA</span><h2>Datos personales</h2></div><span className={`status-badge ${user.status.toLowerCase()}`}>{user.status === 'ACTIVE' ? 'Activo' : 'Inactivo'}</span></div>
|
||||
<div className="form-grid"><label className="field"><span>Nombre</span><input name="firstName" defaultValue={user.firstName} required disabled={!canUpdate} /></label><label className="field"><span>Apellido</span><input name="lastName" defaultValue={user.lastName} required disabled={!canUpdate} /></label><label className="field"><span>Usuario</span><input name="username" defaultValue={user.username} required disabled={!canUpdate} /></label><label className="field"><span>Email</span><input name="email" type="email" defaultValue={user.email ?? ''} disabled={!canUpdate} /></label></div>
|
||||
<div className="panel-heading"><div><span className="eyebrow">PERFIL</span><h2>Datos personales y contacto</h2></div><span className={`status-badge ${user.status.toLowerCase()}`}>{user.status === 'ACTIVE' ? 'Activo' : 'Inactivo'}</span></div>
|
||||
<div className="form-grid">
|
||||
<label className="field"><span>Nombre</span><input name="firstName" defaultValue={user.firstName} required disabled={!canUpdate} /></label>
|
||||
<label className="field"><span>Apellido</span><input name="lastName" defaultValue={user.lastName} required disabled={!canUpdate} /></label>
|
||||
<label className="field"><span>DNI</span><input name="dni" defaultValue={user.dni ?? ''} inputMode="numeric" maxLength={15} disabled={!canUpdate} /></label>
|
||||
<label className="field"><span>Teléfono</span><input name="phone" type="tel" defaultValue={user.phone ?? ''} maxLength={40} disabled={!canUpdate} /></label>
|
||||
<label className="field"><span>Cargo / función</span><input name="jobTitle" defaultValue={user.jobTitle ?? ''} maxLength={160} disabled={!canUpdate} /></label>
|
||||
<label className="field"><span>Legajo / matrícula</span><input name="employeeNumber" defaultValue={user.employeeNumber ?? ''} maxLength={80} disabled={!canUpdate} /></label>
|
||||
<label className="field"><span>Usuario</span><input name="username" defaultValue={user.username} required disabled={!canUpdate} /></label>
|
||||
<label className="field"><span>Email {inspectorSelected && <em>obligatorio para Inspector</em>}</span><input name="email" type="email" defaultValue={user.email ?? ''} required={inspectorSelected} disabled={!canUpdate} /></label>
|
||||
</div>
|
||||
{inspectorSelected && <div className="temporal-notice"><Icon name="mail" /><p><strong>Destinatario del Inspector:</strong> al finalizar una inspección, la documentación se enviará también a <strong>{user.email || 'este email cuando lo completes'}</strong>.</p></div>}
|
||||
<div className="metadata-grid"><div><small>Último acceso</small><strong>{formatDate(user.lastLoginAt)}</strong></div><div><small>Último cambio de clave</small><strong>{formatDate(user.passwordChangedAt)}</strong></div><div><small>Intentos fallidos</small><strong>{user.failedLoginAttempts}</strong></div><div><small>Bloqueado hasta</small><strong>{formatDate(user.lockedUntil)}</strong></div></div>
|
||||
{user.mustChangePassword && <Alert type="info">Este usuario debe cambiar su contraseña temporal en el próximo ingreso.</Alert>}
|
||||
{canUpdate && <div className="form-actions"><button className="button primary" disabled={saving === 'profile'}><Icon name="edit" />{saving === 'profile' ? 'Guardando…' : 'Guardar datos'}</button></div>}
|
||||
@@ -134,7 +167,8 @@ export function UserDetailPage() {
|
||||
<article className="panel form-panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">AUTORIZACIÓN</span><h2>Roles asignados</h2></div><span className="count-pill">{roleIds.length}</span></div>
|
||||
<div className="choice-list">{roles.map((role) => <label className={`choice-card compact ${roleIds.includes(role.id) ? 'selected' : ''}`} key={role.id}><input type="checkbox" checked={roleIds.includes(role.id)} onChange={() => toggleRole(role.id)} disabled={!canAssign} /><span><strong>{role.name}</strong><small>{role.description}</small></span><Icon name="check" /></label>)}</div>
|
||||
{canAssign && <div className="form-actions"><button className="button primary" type="button" onClick={saveRoles} disabled={saving === 'roles'}>{saving === 'roles' ? 'Guardando…' : 'Guardar roles'}</button></div>}
|
||||
{inspectorSelected && !user.email && <Alert>Para guardar el rol Inspector, primero cargá y guardá un email válido.</Alert>}
|
||||
{canAssign && <div className="form-actions"><button className="button primary" type="button" onClick={saveRoles} disabled={saving === 'roles' || (inspectorSelected && !user.email)}>{saving === 'roles' ? 'Guardando…' : 'Guardar roles'}</button></div>}
|
||||
</article>
|
||||
</div>
|
||||
</section>;
|
||||
|
||||
Reference in New Issue
Block a user