feat(admin): add direct client onboarding modal and management directly in app.4ventures.es

This commit is contained in:
Marcos Fargas
2026-09-21 02:26:40 +02:00
parent 3bcdb6379f
commit 66e5e308f4
3 changed files with 491 additions and 44 deletions

View File

@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
export async function POST(req: NextRequest) {
try {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'No autorizado. Inicia sesión.' }, { status: 401 });
}
const userRole = user.app_metadata?.role || user.user_metadata?.role || 'client';
if (userRole !== 'admin' && userRole !== 'team') {
return NextResponse.json({ error: 'Permisos insuficientes. Solo administradores pueden crear clientes.' }, { status: 403 });
}
const body = await req.json();
const { companyName, slug, email, password, role } = body;
if (!companyName || !slug || !email || !password) {
return NextResponse.json({ error: 'Todos los campos son obligatorios.' }, { status: 400 });
}
const sanitizedSlug = slug.toLowerCase().trim().replace(/[^a-z0-9-]/g, '-');
const targetRole = role === 'admin' || role === 'team' ? role : 'client';
const { data, error } = await supabase.rpc('provision_client', {
p_company_name: companyName.trim(),
p_slug: sanitizedSlug,
p_email: email.toLowerCase().trim(),
p_password: password,
p_role: targetRole,
});
if (error) {
console.error('Error in provision_client RPC:', error);
return NextResponse.json({ error: error.message || 'Error al provisionar cliente en base de datos.' }, { status: 500 });
}
return NextResponse.json({ success: true, client: data });
} catch (err: any) {
console.error('Server error creating client:', err);
return NextResponse.json({ error: err?.message || 'Error interno del servidor.' }, { status: 500 });
}
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { Building2, DollarSign, Users, TrendingUp, Layers } from 'lucide-react';
import ClientManagement from '@/components/ClientManagement';
export const revalidate = 0;
@@ -101,50 +102,8 @@ export default async function AgencyPage() {
</div>
</div>
{/* Tenants Table */}
<div className="bg-[#111827] border border-gray-800 rounded-xl overflow-hidden">
<div className="p-5 border-b border-gray-800">
<h3 className="text-base font-bold text-white">Cartera de Clientes Activos</h3>
<p className="text-xs text-gray-400 mt-0.5">
Cada cliente cuenta con aislamiento nativo Row Level Security (RLS) en Postgres.
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm text-gray-300">
<thead className="bg-gray-900/50 text-xs uppercase text-gray-400 border-b border-gray-800">
<tr>
<th className="px-5 py-3">Nombre del Cliente</th>
<th className="px-5 py-3">Tenant Slug</th>
<th className="px-5 py-3">UUID</th>
<th className="px-5 py-3">Acceso Portal</th>
<th className="px-5 py-3">Estado</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800/60">
{tenants.map((t) => (
<tr key={t.id} className="hover:bg-gray-800/30 transition-colors">
<td className="px-5 py-3.5 font-semibold text-white">{t.name}</td>
<td className="px-5 py-3.5 text-xs text-brand-400 font-mono">{t.slug}</td>
<td className="px-5 py-3.5 text-xs text-gray-400 font-mono">{t.id}</td>
<td className="px-5 py-3.5 text-xs">
<a
href={`/dashboard/kpis?tenant=${t.slug}`}
className="text-brand-400 hover:underline inline-flex items-center gap-1"
>
Abrir Portal ↗
</a>
</td>
<td className="px-5 py-3.5 text-xs">
<span className="bg-emerald-950/80 text-emerald-400 px-2 py-0.5 rounded-full border border-emerald-800/40 text-[10px] font-bold uppercase">
Activo
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Interactive Tenants Management with Onboarding Form */}
<ClientManagement initialTenants={tenants} />
</div>
);
}