diff --git a/src/app/api/admin/create-client/route.ts b/src/app/api/admin/create-client/route.ts
new file mode 100644
index 0000000..a4727c3
--- /dev/null
+++ b/src/app/api/admin/create-client/route.ts
@@ -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 });
+ }
+}
diff --git a/src/app/dashboard/agencia/page.tsx b/src/app/dashboard/agencia/page.tsx
index 2df4899..3cdc4c3 100644
--- a/src/app/dashboard/agencia/page.tsx
+++ b/src/app/dashboard/agencia/page.tsx
@@ -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() {
- {/* Tenants Table */}
-
-
-
Cartera de Clientes Activos
-
- Cada cliente cuenta con aislamiento nativo Row Level Security (RLS) en Postgres.
-
-
-
-
-
-
- | Nombre del Cliente |
- Tenant Slug |
- UUID |
- Acceso Portal |
- Estado |
-
-
-
- {tenants.map((t) => (
-
- | {t.name} |
- {t.slug} |
- {t.id} |
-
-
- Abrir Portal ↗
-
- |
-
-
- Activo
-
- |
-
- ))}
-
-
-
-
+ {/* Interactive Tenants Management with Onboarding Form */}
+
);
}
diff --git a/src/components/ClientManagement.tsx b/src/components/ClientManagement.tsx
new file mode 100644
index 0000000..77041a7
--- /dev/null
+++ b/src/components/ClientManagement.tsx
@@ -0,0 +1,442 @@
+'use client';
+
+import React, { useState } from 'react';
+import {
+ Plus,
+ Key,
+ Copy,
+ Check,
+ X,
+ ExternalLink,
+ ShieldCheck,
+ Building2,
+ Mail,
+ Lock,
+ Sparkles,
+ Loader2,
+ CheckCircle2
+} from 'lucide-react';
+
+interface Tenant {
+ id: string;
+ name: string;
+ slug: string;
+ created_at: string;
+}
+
+interface ClientManagementProps {
+ initialTenants: Tenant[];
+}
+
+export default function ClientManagement({ initialTenants }: ClientManagementProps) {
+ const [tenants, setTenants] = useState(initialTenants);
+ const [isOpen, setIsOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [errorMsg, setErrorMsg] = useState(null);
+
+ // Form state
+ const [companyName, setCompanyName] = useState('');
+ const [slug, setSlug] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [role, setRole] = useState<'client' | 'team' | 'admin'>('client');
+ const [autoSlug, setAutoSlug] = useState(true);
+
+ // Created access card state
+ const [createdClient, setCreatedClient] = useState<{
+ company: string;
+ slug: string;
+ email: string;
+ password: string;
+ role: string;
+ } | null>(null);
+ const [copied, setCopied] = useState(false);
+
+ // Auto slug generation from company name
+ const handleNameChange = (val: string) => {
+ setCompanyName(val);
+ if (autoSlug) {
+ const generated = val
+ .toLowerCase()
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/(^-|-$)+/g, '');
+ setSlug(generated);
+ }
+ };
+
+ const generateRandomPassword = () => {
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%&*';
+ let pw = '';
+ for (let i = 0; i < 14; i++) {
+ pw += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ setPassword(pw);
+ };
+
+ const handleCreate = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setErrorMsg(null);
+ setLoading(true);
+
+ try {
+ const res = await fetch('/api/admin/create-client', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ companyName,
+ slug,
+ email,
+ password,
+ role
+ })
+ });
+
+ const data = await res.json();
+
+ if (!res.ok || data.error) {
+ throw new Error(data.error || 'Error al crear cliente');
+ }
+
+ // Add to local state if not exists
+ const newTenant: Tenant = {
+ id: data.client.tenant_id,
+ name: companyName,
+ slug: slug,
+ created_at: new Date().toISOString()
+ };
+
+ setTenants(prev => {
+ const filtered = prev.filter(t => t.slug !== slug);
+ return [...filtered, newTenant];
+ });
+
+ setCreatedClient({
+ company: companyName,
+ slug: slug,
+ email: email,
+ password: password,
+ role: role
+ });
+
+ // Clear input fields
+ setCompanyName('');
+ setSlug('');
+ setEmail('');
+ setPassword('');
+ } catch (err: any) {
+ setErrorMsg(err.message || 'Error inesperado');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const copyCredentials = () => {
+ if (!createdClient) return;
+ const text = `🔐 *Acceso a tu Centro de Mando — 4Ventures*
+Empresa: ${createdClient.company}
+Portal Web: https://app.4ventures.es/login
+Usuario: ${createdClient.email}
+Contraseña: ${createdClient.password}
+Rol: ${createdClient.role === 'client' ? 'Cliente' : 'Agencia'}
+
+Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`;
+
+ navigator.clipboard.writeText(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2500);
+ };
+
+ return (
+
+ {/* Table Header with Action Button */}
+
+
+
+
+ Cartera de Clientes Activos ({tenants.length})
+
+
+ Cada cliente cuenta con aislamiento nativo Row Level Security (RLS) en Postgres.
+
+
+
+
+
+
+ {/* Tenants Table */}
+
+
+
+
+ | Nombre del Cliente |
+ Tenant Slug |
+ UUID |
+ Acceso Portal |
+ Estado |
+
+
+
+ {tenants.map((t) => (
+
+ |
+
+ {t.name}
+ |
+ {t.slug} |
+ {t.id} |
+
+
+ Abrir Portal
+
+ |
+
+
+ Activo
+
+ |
+
+ ))}
+
+
+
+
+ {/* Modal: Onboard New Client */}
+ {isOpen && (
+
+
+ {/* Modal Header */}
+
+
+
+
+
+
+
Alta de Nuevo Cliente
+
Crea el portal corporativo y activa sus credenciales de acceso
+
+
+
+
+
+ {/* Modal Body */}
+
+ {createdClient ? (
+ /* Success Screen with Credentials */
+
+
+
+
+
¡Cliente Creado con Éxito!
+
+ El tenant ha sido configurado en Postgres con aislamiento RLS y el usuario ya puede iniciar sesión.
+
+
+
+
+
+
+ Empresa:
+ {createdClient.company}
+
+
+ URL Acceso:
+ https://app.4ventures.es/login
+
+
+ Email:
+ {createdClient.email}
+
+
+ Contraseña:
+
+ {createdClient.password}
+
+
+
+
+
+
+
+
+
+ ) : (
+ /* Creation Form */
+
+ )}
+
+
+
+ )}
+
+ );
+}