feat(portal): complete masterplan with deterministic budget simulator, RAG assistant, agency cross-tenant view and multi-tenant middleware

This commit is contained in:
Marcos Fargas
2026-09-21 01:51:50 +02:00
parent 3298e4154f
commit f728c33533
7 changed files with 516 additions and 1 deletions

View File

@@ -0,0 +1,150 @@
import React from 'react';
import { Building2, DollarSign, Users, TrendingUp, Layers } from 'lucide-react';
export const revalidate = 0;
export default async function AgencyPage() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://supabase.4ventures.es';
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
const headers = {
apikey: serviceKey,
Authorization: `Bearer ${serviceKey}`
};
let tenants: any[] = [];
let totalSpend = 0;
let totalLeads = 0;
let totalWonDeals = 0;
let totalRevenue = 0;
try {
const [tenantsRes, adRes, leadsRes, dealsRes] = await Promise.all([
fetch(`${supabaseUrl}/rest/v1/tenants?select=*&order=created_at.asc`, { headers, cache: 'no-store' }),
fetch(`${supabaseUrl}/rest/v1/ad_daily?select=spend`, { headers, cache: 'no-store' }),
fetch(`${supabaseUrl}/rest/v1/leads?select=id`, { headers, cache: 'no-store' }),
fetch(`${supabaseUrl}/rest/v1/deals?select=amount&stage=eq.won`, { headers, cache: 'no-store' })
]);
if (tenantsRes.ok) tenants = await tenantsRes.json();
if (adRes.ok) {
const ads = await adRes.json();
totalSpend = ads.reduce((acc: number, curr: any) => acc + Number(curr.spend || 0), 0);
}
if (leadsRes.ok) {
const leads = await leadsRes.json();
totalLeads = leads.length;
}
if (dealsRes.ok) {
const deals = await dealsRes.json();
totalWonDeals = deals.length;
totalRevenue = deals.reduce((acc: number, curr: any) => acc + Number(curr.amount || 0), 0);
}
} catch (e) {
console.error('Error in AgencyPage:', e);
}
const blendedRoas = totalSpend > 0 ? (totalRevenue / totalSpend).toFixed(2) : '0';
return (
<div className="space-y-8">
<div>
<div className="inline-flex items-center gap-2 px-2.5 py-1 rounded-md bg-brand-950/60 border border-brand-800/40 text-brand-400 text-xs font-semibold uppercase tracking-wider mb-2">
<Layers className="w-3.5 h-3.5" /> Vista Global de Agencia (Cross-Tenant)
</div>
<h2 className="text-2xl font-bold tracking-tight text-white">Centro de Mando 4Ventures</h2>
<p className="text-sm text-gray-400 mt-1">
Métricas consolidadas de infraestructura, inversión publicitaria y cartera de clientes.
</p>
</div>
{/* Global Agency KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-[#111827] border border-gray-800 rounded-xl p-5">
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-gray-400 font-medium">Clientes / Tenants</span>
<Building2 className="w-4 h-4 text-brand-400" />
</div>
<p className="text-2xl font-extrabold text-white">{tenants.length}</p>
<p className="text-[11px] text-emerald-400 mt-1">100% aislados vía RLS</p>
</div>
<div className="bg-[#111827] border border-gray-800 rounded-xl p-5">
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-gray-400 font-medium">Inversión Gestionada</span>
<DollarSign className="w-4 h-4 text-blue-400" />
</div>
<p className="text-2xl font-extrabold text-white">
{totalSpend.toLocaleString('es-ES', { minimumFractionDigits: 2 })} €
</p>
<p className="text-[11px] text-gray-400 mt-1">Vía Windsor.ai pipeline</p>
</div>
<div className="bg-[#111827] border border-gray-800 rounded-xl p-5">
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-gray-400 font-medium">Leads Captados</span>
<Users className="w-4 h-4 text-purple-400" />
</div>
<p className="text-2xl font-extrabold text-white">{totalLeads}</p>
<p className="text-[11px] text-gray-400 mt-1">{totalWonDeals} cerrados ganados</p>
</div>
<div className="bg-[#111827] border border-gray-800 rounded-xl p-5">
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-gray-400 font-medium">ROAS Blended Agencia</span>
<TrendingUp className="w-4 h-4 text-emerald-400" />
</div>
<p className="text-2xl font-extrabold text-white">{blendedRoas}x</p>
<p className="text-[11px] text-emerald-400 mt-1">
{totalRevenue.toLocaleString('es-ES', { minimumFractionDigits: 2 })} € generados
</p>
</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>
</div>
);
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { Target, CheckCircle2, TrendingUp } from 'lucide-react';
import BudgetSimulator from '@/components/BudgetSimulator';
export const revalidate = 0;
@@ -103,6 +104,8 @@ export default async function EstrategiaPage() {
</p>
</div>
</div>
<BudgetSimulator />
</div>
);
}

View File

@@ -5,7 +5,8 @@ import {
Target,
FolderGit2,
Bot,
ShieldCheck
ShieldCheck,
Layers
} from 'lucide-react';
export default function DashboardLayout({
@@ -57,6 +58,16 @@ export default function DashboardLayout({
<Bot className="w-4 h-4 text-purple-400" />
Asistente IA Financiero
</Link>
<div className="pt-4 mt-4 border-t border-gray-800/60">
<span className="px-3 text-[10px] font-bold uppercase tracking-wider text-gray-400">Agencia OS</span>
<Link
href="/dashboard/agencia"
className="flex items-center gap-3 px-3 py-2.5 mt-1 rounded-lg text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-800/60 transition-all"
>
<Layers className="w-4 h-4 text-amber-400" />
Cartera Cross-Tenant
</Link>
</div>
</nav>
</div>