diff --git a/mvp/b2c/src/app/admin/usuarios/CrearReformistaForm.tsx b/mvp/b2c/src/app/admin/usuarios/CrearReformistaForm.tsx
new file mode 100644
index 0000000..2490471
--- /dev/null
+++ b/mvp/b2c/src/app/admin/usuarios/CrearReformistaForm.tsx
@@ -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 (
+
+ );
+}
diff --git a/mvp/b2c/src/app/admin/usuarios/actions.ts b/mvp/b2c/src/app/admin/usuarios/actions.ts
new file mode 100644
index 0000000..8ac043e
--- /dev/null
+++ b/mvp/b2c/src/app/admin/usuarios/actions.ts
@@ -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 {
+ 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');
+}
diff --git a/mvp/b2c/src/app/admin/usuarios/page.tsx b/mvp/b2c/src/app/admin/usuarios/page.tsx
new file mode 100644
index 0000000..1ff6517
--- /dev/null
+++ b/mvp/b2c/src/app/admin/usuarios/page.tsx
@@ -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 (
+
+
Usuarios
+
+
+
+
+ | Email | Rol |
+ Empresa | Estado | |
+
+
+ {users.map((u) => (
+
+ | {u.email} |
+ {u.role} |
+ {u.tenantId ? empresaDe.get(u.tenantId) ?? '—' : '—'} |
+ {u.status} |
+
+ {u.role !== 'admin' && (
+
+ )}
+ |
+
+ ))}
+
+
+
+
+ );
+}