feat(docker): añadir Dockerfile para contenerizar la aplicación
This commit is contained in:
252
src/pages/Users.tsx
Normal file
252
src/pages/Users.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { usuariosApi } from '@/api/usuarios';
|
||||
import { rolesApi } from '@/api/roles';
|
||||
import type { ActualizarUsuarioBody, Usuario } from '@/types';
|
||||
import { PageHeader } from '@/components/PageHeader';
|
||||
import { DataTable, type Column } from '@/components/DataTable';
|
||||
import { Modal } from '@/components/Modal';
|
||||
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
||||
import { ErrorAlert } from '@/components/ErrorAlert';
|
||||
import {
|
||||
Field,
|
||||
Input,
|
||||
Select,
|
||||
Checkbox,
|
||||
btnPrimary,
|
||||
btnSecondary,
|
||||
btnDanger,
|
||||
} from '@/components/form/Field';
|
||||
|
||||
export function UsersPage() {
|
||||
const qc = useQueryClient();
|
||||
const [editUser, setEditUser] = useState<Usuario | null>(null);
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
const [formError, setFormError] = useState('');
|
||||
|
||||
const usuarios = useQuery({ queryKey: ['usuarios'], queryFn: usuariosApi.list });
|
||||
const pendientes = useQuery({
|
||||
queryKey: ['usuarios-pendientes'],
|
||||
queryFn: usuariosApi.pendientes,
|
||||
});
|
||||
const roles = useQuery({ queryKey: ['roles'], queryFn: rolesApi.list });
|
||||
|
||||
const aceptar = useMutation({
|
||||
mutationFn: (id: number) => usuariosApi.aceptar(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['usuarios'] });
|
||||
qc.invalidateQueries({ queryKey: ['usuarios-pendientes'] });
|
||||
qc.invalidateQueries({ queryKey: ['dashboard-stats'] });
|
||||
},
|
||||
});
|
||||
|
||||
const update = useMutation({
|
||||
mutationFn: ({ id, body }: { id: number; body: ActualizarUsuarioBody }) =>
|
||||
usuariosApi.update(id, body),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['usuarios'] });
|
||||
qc.invalidateQueries({ queryKey: ['usuarios-pendientes'] });
|
||||
setEditUser(null);
|
||||
},
|
||||
onError: (e: Error) => setFormError(e.message),
|
||||
});
|
||||
|
||||
const remove = useMutation({
|
||||
mutationFn: (id: number) => usuariosApi.delete(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['usuarios'] });
|
||||
setDeleteId(null);
|
||||
},
|
||||
});
|
||||
|
||||
const columns: Column<Usuario>[] = [
|
||||
{ key: 'id', header: 'ID', render: (u) => u.id },
|
||||
{ key: 'email', header: 'Email', render: (u) => u.username },
|
||||
{ key: 'nombre', header: 'Nombre', render: (u) => u.nombre },
|
||||
{
|
||||
key: 'rol',
|
||||
header: 'Rol',
|
||||
render: (u) => u.rol?.name ?? u.roles ?? '—',
|
||||
},
|
||||
{
|
||||
key: 'aprobado',
|
||||
header: 'Aprobado',
|
||||
render: (u) => (u.aprobado ? 'Sí' : 'No'),
|
||||
},
|
||||
{
|
||||
key: 'visible',
|
||||
header: 'Visible',
|
||||
render: (u) => (u.visible ? 'Sí' : 'No'),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: '',
|
||||
render: (u) => (
|
||||
<div className="flex gap-2">
|
||||
<button type="button" className={btnSecondary} onClick={() => setEditUser(u)}>
|
||||
Editar
|
||||
</button>
|
||||
<button type="button" className={btnDanger} onClick={() => setDeleteId(u.id)}>
|
||||
Eliminar
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Usuarios"
|
||||
description="Gestión de cuentas y aprobaciones"
|
||||
/>
|
||||
|
||||
{pendientes.data && pendientes.data.length > 0 && (
|
||||
<section className="mb-8 rounded-xl border border-amber-500/30 bg-amber-950/20 p-5">
|
||||
<h2 className="mb-3 font-semibold text-amber-200">
|
||||
Pendientes de aprobación ({pendientes.data.length})
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{pendientes.data.map((u) => (
|
||||
<li
|
||||
key={u.id}
|
||||
className="flex flex-wrap items-center justify-between gap-2 rounded-lg bg-surface-raised/80 px-4 py-3"
|
||||
>
|
||||
<span>
|
||||
<strong>{u.nombre}</strong> — {u.username}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={btnPrimary}
|
||||
disabled={aceptar.isPending}
|
||||
onClick={() => aceptar.mutate(u.id)}
|
||||
>
|
||||
Aprobar
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={usuarios.data ?? []}
|
||||
keyExtractor={(u) => u.id}
|
||||
loading={usuarios.isLoading}
|
||||
/>
|
||||
|
||||
{editUser && (
|
||||
<UserFormModal
|
||||
user={editUser}
|
||||
roles={roles.data ?? []}
|
||||
error={formError}
|
||||
loading={update.isPending}
|
||||
onClose={() => {
|
||||
setEditUser(null);
|
||||
setFormError('');
|
||||
}}
|
||||
onSave={(body) => update.mutate({ id: editUser.id, body })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
open={deleteId != null}
|
||||
title="Eliminar usuario"
|
||||
message="¿Confirma eliminar este usuario? Esta acción no se puede deshacer."
|
||||
onConfirm={() => deleteId != null && remove.mutate(deleteId)}
|
||||
onCancel={() => setDeleteId(null)}
|
||||
loading={remove.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserFormModal({
|
||||
user,
|
||||
roles,
|
||||
error,
|
||||
loading,
|
||||
onClose,
|
||||
onSave,
|
||||
}: {
|
||||
user: Usuario;
|
||||
roles: { id: number; name: string }[];
|
||||
error: string;
|
||||
loading: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (body: ActualizarUsuarioBody) => void;
|
||||
}) {
|
||||
const [nombre, setNombre] = useState(user.nombre);
|
||||
const [ci, setCi] = useState(String(user.ci));
|
||||
const [celular, setCelular] = useState(String(user.celular));
|
||||
const [aprobado, setAprobado] = useState(user.aprobado);
|
||||
const [visible, setVisible] = useState(user.visible);
|
||||
const [rolId, setRolId] = useState(String(user.rol?.id ?? ''));
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
return (
|
||||
<Modal open title={`Editar: ${user.username}`} onClose={onClose}>
|
||||
{error && <div className="mb-4"><ErrorAlert message={error} /></div>}
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const body: ActualizarUsuarioBody = {
|
||||
nombre,
|
||||
ci: Number(ci),
|
||||
celular: Number(celular),
|
||||
aprobado,
|
||||
visible,
|
||||
rol: rolId ? Number(rolId) : undefined,
|
||||
};
|
||||
if (password) body.password = password;
|
||||
onSave(body);
|
||||
}}
|
||||
>
|
||||
<Field label="Nombre">
|
||||
<Input value={nombre} onChange={(e) => setNombre(e.target.value)} required />
|
||||
</Field>
|
||||
<Field label="CI">
|
||||
<Input type="number" value={ci} onChange={(e) => setCi(e.target.value)} required />
|
||||
</Field>
|
||||
<Field label="Celular">
|
||||
<Input
|
||||
type="number"
|
||||
value={celular}
|
||||
onChange={(e) => setCelular(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Rol (tabla roles)">
|
||||
<Select value={rolId} onChange={(e) => setRolId(e.target.value)}>
|
||||
<option value="">— Sin cambio —</option>
|
||||
{roles.map((r) => (
|
||||
<option key={r.id} value={r.id}>
|
||||
{r.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="Nueva contraseña (opcional)">
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Field>
|
||||
<Checkbox label="Aprobado" checked={aprobado} onChange={(e) => setAprobado(e.target.checked)} />
|
||||
<Checkbox label="Visible" checked={visible} onChange={(e) => setVisible(e.target.checked)} />
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<button type="button" className={btnSecondary} onClick={onClose}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" className={btnPrimary} disabled={loading}>
|
||||
Guardar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user