358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { AppShell } from "@/components/AppShell";
|
|
import { useAuth, useCan } from "@/lib/abilities";
|
|
import { ROLE_LABEL, ROLES_DESC } from "@/lib/labels";
|
|
import {
|
|
createUser,
|
|
deleteUser,
|
|
listUsers,
|
|
resetUserPassword,
|
|
updateUser,
|
|
} from "@/lib/api";
|
|
import type { Role, UserRow } from "@/lib/types";
|
|
|
|
export default function UsuariosPage() {
|
|
return (
|
|
<AppShell>
|
|
<UsuariosAdmin />
|
|
</AppShell>
|
|
);
|
|
}
|
|
|
|
type FormState = {
|
|
name: string;
|
|
email: string;
|
|
password: string;
|
|
role: Role;
|
|
active: boolean;
|
|
};
|
|
|
|
const EMPTY_FORM: FormState = {
|
|
name: "",
|
|
email: "",
|
|
password: "",
|
|
role: "STAFF",
|
|
active: true,
|
|
};
|
|
|
|
function UsuariosAdmin() {
|
|
const me = useAuth();
|
|
const allowed = useCan("user:manage");
|
|
|
|
const [users, setUsers] = useState<UserRow[] | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [notice, setNotice] = useState<string | null>(null);
|
|
|
|
// null = create mode; a user id = editing that row.
|
|
const [editingId, setEditingId] = useState<string | null>(null);
|
|
const [form, setForm] = useState<FormState>(EMPTY_FORM);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
// Inline "reset password" target + value.
|
|
const [pwTarget, setPwTarget] = useState<string | null>(null);
|
|
const [pwValue, setPwValue] = useState("");
|
|
|
|
function refresh() {
|
|
listUsers()
|
|
.then(setUsers)
|
|
.catch((e) => setError(e?.message ?? "No se pudieron cargar los usuarios."));
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (allowed) refresh();
|
|
}, [allowed]);
|
|
|
|
if (!allowed) {
|
|
return (
|
|
<div className="page-head">
|
|
<h1 className="page-title">Usuarios</h1>
|
|
<div className="state-box state-error">
|
|
No tiene permisos para administrar usuarios.
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function startCreate() {
|
|
setEditingId(null);
|
|
setForm(EMPTY_FORM);
|
|
setNotice(null);
|
|
setError(null);
|
|
}
|
|
|
|
function startEdit(u: UserRow) {
|
|
setEditingId(u.id);
|
|
setForm({ name: u.name, email: u.email, password: "", role: u.role, active: u.active });
|
|
setNotice(null);
|
|
setError(null);
|
|
}
|
|
|
|
async function submit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setSaving(true);
|
|
setError(null);
|
|
setNotice(null);
|
|
try {
|
|
if (editingId) {
|
|
await updateUser(editingId, {
|
|
name: form.name,
|
|
email: form.email,
|
|
role: form.role,
|
|
active: form.active,
|
|
});
|
|
setNotice("Usuario actualizado.");
|
|
} else {
|
|
await createUser({
|
|
name: form.name,
|
|
email: form.email,
|
|
password: form.password,
|
|
role: form.role,
|
|
active: form.active,
|
|
});
|
|
setNotice("Usuario creado.");
|
|
}
|
|
startCreate();
|
|
refresh();
|
|
} catch (e2) {
|
|
setError((e2 as Error)?.message ?? "No se pudo guardar el usuario.");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
async function submitPassword(id: string) {
|
|
setError(null);
|
|
try {
|
|
await resetUserPassword(id, pwValue);
|
|
setPwTarget(null);
|
|
setPwValue("");
|
|
setNotice("Contraseña restablecida.");
|
|
} catch (e) {
|
|
setError((e as Error)?.message ?? "No se pudo restablecer la contraseña.");
|
|
}
|
|
}
|
|
|
|
async function submitDelete(u: UserRow) {
|
|
if (!window.confirm(`¿Eliminar al usuario "${u.name}"? Esta acción no se puede deshacer.`)) {
|
|
return;
|
|
}
|
|
setError(null);
|
|
setNotice(null);
|
|
try {
|
|
await deleteUser(u.id);
|
|
if (editingId === u.id) startCreate();
|
|
setNotice("Usuario eliminado.");
|
|
refresh();
|
|
} catch (e) {
|
|
setError((e as Error)?.message ?? "No se pudo eliminar el usuario.");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="page-head">
|
|
<h1 className="page-title">Usuarios</h1>
|
|
</div>
|
|
|
|
{error && <div className="state-box state-error">{error}</div>}
|
|
{notice && <div className="state-box">{notice}</div>}
|
|
|
|
{/* Create / edit form */}
|
|
<div className="card" style={{ padding: 20, marginBottom: 20 }}>
|
|
<h2 className="section-title" style={{ marginBottom: 4 }}>
|
|
{editingId ? "Editar usuario" : "Nuevo usuario"}
|
|
</h2>
|
|
<p className="inline-form-note">
|
|
El rol define el acceso: Solo lectura no puede escribir; Personal y
|
|
superior sí. Administrador gestiona usuarios.
|
|
</p>
|
|
<form onSubmit={submit}>
|
|
<div className="form-grid">
|
|
<label className="field">
|
|
<span className="field-label">Nombre</span>
|
|
<input
|
|
className="input"
|
|
required
|
|
value={form.name}
|
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
|
/>
|
|
</label>
|
|
<label className="field">
|
|
<span className="field-label">Correo</span>
|
|
<input
|
|
className="input"
|
|
type="email"
|
|
required
|
|
value={form.email}
|
|
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
|
/>
|
|
</label>
|
|
{!editingId && (
|
|
<label className="field">
|
|
<span className="field-label">Contraseña (mín. 8)</span>
|
|
<input
|
|
className="input"
|
|
type="password"
|
|
required
|
|
minLength={8}
|
|
value={form.password}
|
|
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
|
/>
|
|
</label>
|
|
)}
|
|
<label className="field">
|
|
<span className="field-label">Rol</span>
|
|
<select
|
|
className="select"
|
|
value={form.role}
|
|
onChange={(e) => setForm({ ...form, role: e.target.value as Role })}
|
|
>
|
|
{ROLES_DESC.map((r) => (
|
|
<option key={r} value={r}>
|
|
{ROLE_LABEL[r]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="field" style={{ justifyContent: "flex-end" }}>
|
|
<span className="field-label">Activo</span>
|
|
<input
|
|
type="checkbox"
|
|
checked={form.active}
|
|
disabled={editingId === me?.id}
|
|
onChange={(e) => setForm({ ...form, active: e.target.checked })}
|
|
/>
|
|
</label>
|
|
</div>
|
|
<div className="form-actions">
|
|
<button type="submit" className="btn btn-primary" disabled={saving}>
|
|
{saving ? "Guardando…" : editingId ? "Guardar cambios" : "Crear usuario"}
|
|
</button>
|
|
{editingId && (
|
|
<button type="button" className="btn btn-outline" onClick={startCreate}>
|
|
Cancelar
|
|
</button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{/* List */}
|
|
<div className="card">
|
|
{users === null ? (
|
|
<div className="empty-inline">
|
|
<span className="spinner" aria-label="Cargando" />
|
|
</div>
|
|
) : users.length === 0 ? (
|
|
<div className="empty-inline">Sin usuarios.</div>
|
|
) : (
|
|
<div className="tx-scroll">
|
|
<table className="tx-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Nombre</th>
|
|
<th>Correo</th>
|
|
<th>Rol</th>
|
|
<th>Estado</th>
|
|
<th className="num">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{users.map((u) => (
|
|
<tr key={u.id}>
|
|
<td>
|
|
{u.name}
|
|
{u.id === me?.id && (
|
|
<span className="muted"> (usted)</span>
|
|
)}
|
|
</td>
|
|
<td className="mono">{u.email}</td>
|
|
<td>
|
|
<span className="badge badge-neutral">{ROLE_LABEL[u.role]}</span>
|
|
</td>
|
|
<td>
|
|
<span
|
|
className={`badge ${u.active ? "badge-positive" : "badge-negative"}`}
|
|
>
|
|
{u.active ? "Activo" : "Inactivo"}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
{pwTarget === u.id ? (
|
|
<div className="row-actions">
|
|
<input
|
|
className="input"
|
|
type="password"
|
|
placeholder="Nueva contraseña"
|
|
minLength={8}
|
|
value={pwValue}
|
|
onChange={(e) => setPwValue(e.target.value)}
|
|
style={{ maxWidth: 180 }}
|
|
/>
|
|
<button
|
|
className="btn btn-primary"
|
|
type="button"
|
|
disabled={pwValue.length < 8}
|
|
onClick={() => submitPassword(u.id)}
|
|
>
|
|
Guardar
|
|
</button>
|
|
<button
|
|
className="btn btn-ghost"
|
|
type="button"
|
|
onClick={() => {
|
|
setPwTarget(null);
|
|
setPwValue("");
|
|
}}
|
|
>
|
|
Cancelar
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="row-actions">
|
|
<button
|
|
className="btn btn-outline"
|
|
type="button"
|
|
onClick={() => startEdit(u)}
|
|
>
|
|
Editar
|
|
</button>
|
|
<button
|
|
className="btn btn-ghost"
|
|
type="button"
|
|
onClick={() => {
|
|
setPwTarget(u.id);
|
|
setPwValue("");
|
|
}}
|
|
>
|
|
Contraseña
|
|
</button>
|
|
<button
|
|
className="btn btn-ghost btn-danger"
|
|
type="button"
|
|
disabled={u.id === me?.id}
|
|
title={
|
|
u.id === me?.id
|
|
? "No puede eliminar su propia cuenta"
|
|
: "Eliminar usuario"
|
|
}
|
|
onClick={() => submitDelete(u)}
|
|
>
|
|
Eliminar
|
|
</button>
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|