Add gestión de usuarios en el admin

This commit is contained in:
Carlos Narro
2026-05-30 19:57:10 +02:00
parent 6f86334c8a
commit 07d41e1f6b
3 changed files with 106 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
'use client';
import { useActionState } from 'react';
import { crearReformista } from './actions';
export function CrearReformistaForm() {
const [error, action, pending] = useActionState(crearReformista, null);
return (
<form action={action} className="bg-white border border-gray-200 rounded-xl p-5 grid grid-cols-1 md:grid-cols-2 gap-3">
<h2 className="md:col-span-2 font-bold text-black">Crear reformista</h2>
<input name="nombre" placeholder="Nombre" required className="border border-gray-300 rounded-md px-3 py-2 text-sm" />
<input name="empresa" placeholder="Empresa" required className="border border-gray-300 rounded-md px-3 py-2 text-sm" />
<input name="email" type="email" placeholder="Email" required className="border border-gray-300 rounded-md px-3 py-2 text-sm" />
<input name="provincia" placeholder="Provincia" required className="border border-gray-300 rounded-md px-3 py-2 text-sm" />
<input name="password" type="password" placeholder="Contraseña (mín. 8)" required className="border border-gray-300 rounded-md px-3 py-2 text-sm" />
{error && <p className="md:col-span-2 text-sm text-red-600">{error}</p>}
<button type="submit" disabled={pending} className="md:col-span-2 bg-black text-white rounded-md py-2 font-semibold disabled:opacity-60">
{pending ? 'Creando…' : 'Crear reformista'}
</button>
</form>
);
}

View File

@@ -0,0 +1,39 @@
'use server';
import { revalidatePath } from 'next/cache';
import { requireAdmin } from '@/lib/auth/current-user';
import { signupSchema, slugify } from '@/lib/validation/signup';
import { getUserByEmail, createTenantWithOwner, slugDisponible } from '@/db/auth-queries';
import { setUserStatus } from '@/db/admin-queries';
import { hashPassword } from '@/lib/auth/password';
export async function crearReformista(_prev: string | null, formData: FormData): Promise<string | null> {
await requireAdmin();
const parsed = signupSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return parsed.error.issues[0]?.message ?? 'Datos no válidos.';
const data = parsed.data;
if (await getUserByEmail(data.email)) return 'Ya existe una cuenta con ese email.';
let slug = slugify(data.empresa);
let n = 1;
while (!(await slugDisponible(slug))) slug = `${slugify(data.empresa)}-${++n}`;
await createTenantWithOwner({
nombreEmpresa: data.empresa,
slug,
provincia: data.provincia,
email: data.email,
passwordHash: await hashPassword(data.password),
nombre: data.nombre,
});
revalidatePath('/admin/usuarios');
return null;
}
export async function toggleUsuario(formData: FormData) {
await requireAdmin();
const userId = String(formData.get('userId'));
const next = String(formData.get('next')) as 'activo' | 'deshabilitado';
await setUserStatus(userId, next);
revalidatePath('/admin/usuarios');
}

View File

@@ -0,0 +1,45 @@
import { listUsers, listTenants } from '@/db/admin-queries';
import { toggleUsuario } from './actions';
import { CrearReformistaForm } from './CrearReformistaForm';
export const dynamic = 'force-dynamic';
export default async function UsuariosPage() {
const [users, tenants] = await Promise.all([listUsers(), listTenants()]);
const empresaDe = new Map(tenants.map((t) => [t.id, t.nombreEmpresa]));
return (
<div className="flex flex-col gap-8">
<h1 className="text-2xl font-black tracking-tight text-black">Usuarios</h1>
<CrearReformistaForm />
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden">
<table className="w-full text-sm">
<thead><tr className="border-b border-gray-200 text-left text-xs uppercase tracking-wide text-gray-400">
<th className="px-4 py-3">Email</th><th className="px-4 py-3">Rol</th>
<th className="px-4 py-3">Empresa</th><th className="px-4 py-3">Estado</th><th className="px-4 py-3"></th>
</tr></thead>
<tbody>
{users.map((u) => (
<tr key={u.id} className="border-b border-gray-100 last:border-0">
<td className="px-4 py-3 font-medium text-black">{u.email}</td>
<td className="px-4 py-3 text-gray-600">{u.role}</td>
<td className="px-4 py-3 text-gray-600">{u.tenantId ? empresaDe.get(u.tenantId) ?? '—' : '—'}</td>
<td className="px-4 py-3 text-gray-600">{u.status}</td>
<td className="px-4 py-3 text-right">
{u.role !== 'admin' && (
<form action={toggleUsuario}>
<input type="hidden" name="userId" value={u.id} />
<input type="hidden" name="next" value={u.status === 'activo' ? 'deshabilitado' : 'activo'} />
<button type="submit" className="text-xs font-medium text-gray-500 hover:text-black">
{u.status === 'activo' ? 'Deshabilitar' : 'Habilitar'}
</button>
</form>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}