Files
dh-inspeccion-v2/web-v2/src/pages/UsersPage.tsx
T

74 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { SearchableSelect } from '../components/SearchableSelect';
import { useEffect, useState } from 'react';
import type { FormEvent } from 'react';
import { Link, useSearchParams } from 'react-router';
import { PermissionGate } from '../auth/PermissionGate';
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../components/Feedback';
import { Icon } from '../components/Icon';
import { listUsers } from '../lib/api';
import type { AdministrativeUser, PageMeta } from '../lib/api';
import { formatDate, initials } from '../lib/format';
export function UsersPage() {
const [urlParams, setUrlParams] = useSearchParams();
const [users, setUsers] = useState<AdministrativeUser[]>([]);
const [meta, setMeta] = useState<PageMeta>({ page: 1, pageSize: 20, total: 0, totalPages: 0 });
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [draftSearch, setDraftSearch] = useState(urlParams.get('search') ?? '');
const search = urlParams.get('search') ?? '';
const rawStatus = urlParams.get('status');
const status = rawStatus === 'ACTIVE' || rawStatus === 'INACTIVE' ? rawStatus : '';
const page = Math.max(1, Number(urlParams.get('page') ?? 1) || 1);
useEffect(() => {
setLoading(true);
setError('');
listUsers({ page, pageSize: 20, search, status })
.then((response) => { setUsers(response.data); setMeta(response.meta); })
.catch((requestError) => setError(errorMessage(requestError)))
.finally(() => setLoading(false));
}, [page, search, status]);
const applyFilters = (event: FormEvent) => {
event.preventDefault();
const next = new URLSearchParams();
if (draftSearch.trim()) next.set('search', draftSearch.trim());
if (status) next.set('status', status);
setUrlParams(next);
};
const setStatus = (value: string) => {
const next = new URLSearchParams(urlParams);
value ? next.set('status', value) : next.delete('status');
next.delete('page');
setUrlParams(next);
};
const setPage = (value: number) => {
const next = new URLSearchParams(urlParams);
value > 1 ? next.set('page', String(value)) : next.delete('page');
setUrlParams(next);
};
return (
<section>
<div className="page-heading"><div><span className="eyebrow">ADMINISTRACIÓN</span><h1>Usuarios</h1><p>Gestioná accesos, estados y roles del sistema.</p></div><PermissionGate permission="users.create"><Link className="button primary" to="/admin/users/new"><Icon name="plus" />Nuevo usuario</Link></PermissionGate></div>
<form className="toolbar" onSubmit={applyFilters}>
<label className="search-field"><Icon name="search" /><input value={draftSearch} onChange={(event) => setDraftSearch(event.target.value)} placeholder="Buscar por nombre, usuario o email" /><button type="submit">Buscar</button></label>
<label className="select-field"><span>Estado</span><SearchableSelect value={status} onChange={(event) => setStatus(event.target.value)}><option value="">Todos</option><option value="ACTIVE">Activos</option><option value="INACTIVE">Inactivos</option></SearchableSelect></label>
</form>
{error && <Alert>{error}</Alert>}
{loading ? <LoadingBlock label="Cargando usuarios…" /> : users.length === 0 ? <EmptyState title="Sin resultados" text="No encontramos usuarios con los filtros seleccionados." /> : <div className="table-panel">
<div className="table-summary"><strong>{meta.total} usuario{meta.total === 1 ? '' : 's'}</strong><span>Página {meta.page} de {Math.max(meta.totalPages, 1)}</span></div>
<div className="table-scroll"><table><thead><tr><th>Usuario</th><th>Roles</th><th>Estado</th><th>Último acceso</th><th aria-label="Acciones" /></tr></thead><tbody>
{users.map((user) => <tr key={user.id}><td><div className="person-cell"><span className="mini-avatar">{initials(user.firstName, user.lastName)}</span><div><strong>{user.firstName} {user.lastName}</strong><small>@{user.username}{user.email ? ` · ${user.email}` : ''}</small></div></div></td><td><div className="tag-list">{user.roles.length ? user.roles.map((role) => <span className="tag" key={role.id}>{role.name}</span>) : <span className="muted">Sin rol</span>}</div></td><td><span className={`status-badge ${user.status.toLowerCase()}`}>{user.status === 'ACTIVE' ? 'Activo' : 'Inactivo'}</span>{user.mustChangePassword && <span className="inline-note">Clave temporal</span>}</td><td>{formatDate(user.lastLoginAt)}</td><td className="action-cell"><Link className="icon-button" to={`/admin/users/${user.id}`} aria-label={`Abrir ${user.username}`}><Icon name="chevron" /></Link></td></tr>)}
</tbody></table></div>
<div className="pagination"><button className="button secondary" disabled={page <= 1} onClick={() => setPage(page - 1)}>Anterior</button><span>{meta.total ? `${(page - 1) * meta.pageSize + 1}${Math.min(page * meta.pageSize, meta.total)} de ${meta.total}` : '0 resultados'}</span><button className="button secondary" disabled={page >= meta.totalPages} onClick={() => setPage(page + 1)}>Siguiente</button></div>
</div>}
</section>
);
}