diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index ab4d968..f02a311 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -19,6 +19,79 @@ export async function POST(req: NextRequest) { 'Content-Type': 'application/json' }; + // 0. Intención: Simulación Determinista "¿Qué pasa si...?" + if (userMessage.includes('simul') || userMessage.includes('invierto') || userMessage.includes('pasa si') || userMessage.includes('proyec')) { + try { + // Extraer importe numérico + const matchNumber = userMessage.match(/(\d+[\d\.,]*)/); + let amount = 1000; + if (matchNumber) { + amount = parseFloat(matchNumber[1].replace(/\./g, '').replace(',', '.')); + } + + let channel: string | null = null; + if (userMessage.includes('meta') || userMessage.includes('facebook') || userMessage.includes('instagram')) channel = 'meta'; + else if (userMessage.includes('google')) channel = 'google'; + else if (userMessage.includes('tiktok')) channel = 'tiktok'; + + const res = await fetch(`${supabaseUrl}/rest/v1/rpc/simulate_budget`, { + method: 'POST', + headers, + body: JSON.stringify({ + p_tenant_id: tenantId, + p_additional_spend: amount, + p_target_channel: channel + }) + }); + + if (res.ok) { + const sim = await res.json(); + const p10 = sim.conservative_p10 || {}; + const p50 = sim.expected_p50 || {}; + const p90 = sim.optimistic_p90 || {}; + const base = sim.baseline || {}; + + const text = `🔮 **Simulación de Inversión Adicional (${amount.toLocaleString('es-ES')} € en ${sim.target_channel.toUpperCase()}):**\n\n` + + `*Métricas Base Reales:* CPL ${base.historical_cpl} €, Tasa de Cierre ${base.conversion_rate_pct}%, Ticket Medio ${Number(base.avg_ticket || 0).toLocaleString('es-ES')} €.\n\n` + + `- **Escenario Esperado (p50):** +${p50.leads} leads, +${p50.deals} clientes, **+${Number(p50.revenue || 0).toLocaleString('es-ES')} €** (ROAS ${p50.roas}x)\n` + + `- **Escenario Conservador (p10):** +${p10.leads} leads, +${p10.deals} clientes, **+${Number(p10.revenue || 0).toLocaleString('es-ES')} €** (ROAS ${p10.roas}x)\n` + + `- **Escenario Optimista (p90):** +${p90.leads} leads, +${p90.deals} clientes, **+${Number(p90.revenue || 0).toLocaleString('es-ES')} €** (ROAS ${p90.roas}x)\n\n` + + `*Proyección calculada matemáticamente sin alucinaciones con la RPC simulate_budget.*`; + return Response.json([{ text }]); + } + } catch (e) { + console.error('Error in simulate_budget chat:', e); + } + } + + // 0.5 Intención: RAG Documental Híbrido (Documentos / Copys / Informes) + if (userMessage.includes('document') || userMessage.includes('copy') || userMessage.includes('informe') || userMessage.includes('brief') || userMessage.includes('contenido') || userMessage.includes('activo')) { + try { + const res = await fetch(`${supabaseUrl}/rest/v1/rpc/match_documents`, { + method: 'POST', + headers, + body: JSON.stringify({ + p_tenant_id: tenantId, + p_query_text: userMessage, + p_match_count: 3 + }) + }); + if (res.ok) { + const matches = await res.json(); + if (Array.isArray(matches) && matches.length > 0) { + const excerpts = matches.map((m: any) => + `📄 **${m.document_title}** (${m.document_type.toUpperCase()}):\n> ${m.chunk_text.replace(/\n/g, '\n> ')}` + ).join('\n\n'); + + const text = `📚 **Fragmentos Recuperados de la Biblioteca de Activos (RAG Híbrido):**\n\n${excerpts}\n\n*Fuente: Búsqueda vectorial y de texto completo en document_chunks.*`; + return Response.json([{ text }]); + } + } + } catch (e) { + console.error('Error in match_documents chat:', e); + } + } + // 1. Intención: Objetivos vs Real / Estrategia if (userMessage.includes('objetivo') || userMessage.includes('meta') || userMessage.includes('factura') || userMessage.includes('estrategia')) { try { diff --git a/src/app/api/simulate/route.ts b/src/app/api/simulate/route.ts new file mode 100644 index 0000000..df69e5a --- /dev/null +++ b/src/app/api/simulate/route.ts @@ -0,0 +1,37 @@ +import { NextRequest } from 'next/server'; + +export async function POST(req: NextRequest) { + 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 tenantId = process.env.DEFAULT_TENANT_ID || '0b3c5fc0-6901-4caf-b26a-b6a0201d8541'; + + try { + const body = await req.json(); + const spend = parseFloat(body.spend || 1000); + const channel = body.channel || 'meta'; + + const res = await fetch(`${supabaseUrl}/rest/v1/rpc/simulate_budget`, { + method: 'POST', + headers: { + apikey: serviceKey, + Authorization: `Bearer ${serviceKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + p_tenant_id: tenantId, + p_additional_spend: spend, + p_target_channel: channel === 'blended' ? null : channel + }) + }); + + if (res.ok) { + const data = await res.json(); + return Response.json(data); + } else { + const err = await res.text(); + return Response.json({ error: err }, { status: 500 }); + } + } catch (e: any) { + return Response.json({ error: e.message }, { status: 500 }); + } +} diff --git a/src/app/dashboard/agencia/page.tsx b/src/app/dashboard/agencia/page.tsx new file mode 100644 index 0000000..2df4899 --- /dev/null +++ b/src/app/dashboard/agencia/page.tsx @@ -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 ( +
+ Métricas consolidadas de infraestructura, inversión publicitaria y cartera de clientes. +
+{tenants.length}
+100% aislados vía RLS
++ {totalSpend.toLocaleString('es-ES', { minimumFractionDigits: 2 })} € +
+Vía Windsor.ai pipeline
+{totalLeads}
+{totalWonDeals} cerrados ganados
+{blendedRoas}x
++ {totalRevenue.toLocaleString('es-ES', { minimumFractionDigits: 2 })} € generados +
++ Cada cliente cuenta con aislamiento nativo Row Level Security (RLS) en Postgres. +
+| Nombre del Cliente | +Tenant Slug | +UUID | +Acceso Portal | +Estado | +
|---|---|---|---|---|
| {t.name} | +{t.slug} | +{t.id} | ++ + Abrir Portal ↗ + + | ++ + Activo + + | +
+ Proyección matemática basada en el histórico de CPL, tasa de cierre y ticket medio en Supabase. +
++ +{Number(p10.revenue || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} € +
++ ROAS {p10.roas || 0}x +
++ +{Number(p50.revenue || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} € +
++ ROAS {p50.roas || 0}x +
++ +{Number(p90.revenue || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} € +
++ ROAS {p90.roas || 0}x +
+