Add gestión de usuarios en el admin
This commit is contained in:
22
mvp/b2c/src/app/admin/usuarios/CrearReformistaForm.tsx
Normal file
22
mvp/b2c/src/app/admin/usuarios/CrearReformistaForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
39
mvp/b2c/src/app/admin/usuarios/actions.ts
Normal file
39
mvp/b2c/src/app/admin/usuarios/actions.ts
Normal 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');
|
||||
}
|
||||
45
mvp/b2c/src/app/admin/usuarios/page.tsx
Normal file
45
mvp/b2c/src/app/admin/usuarios/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user