fix: bind Windsor KPIs block to Contexto tenant

Stop hardcoding Blue Parrot branding/connectors so switching clients
in the portfolio selector updates labels and integration chips.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-05 18:10:17 +02:00
parent ce6dc4e0f0
commit 78bd80079d
2 changed files with 70 additions and 41 deletions

View File

@@ -739,7 +739,7 @@ export default function MetricsDashboard({
</p> </p>
</section> </section>
{/* Windsor.ai Data for blueparrotschool.com (Directly below Cruce por canal) */} {/* Windsor.ai Data — follows Contexto tenant */}
<WindsorAiSection <WindsorAiSection
tenantId={tenantId} tenantId={tenantId}
label={label} label={label}

View File

@@ -35,8 +35,6 @@ import {
import { OrganicDailyRow } from '@/lib/organic'; import { OrganicDailyRow } from '@/lib/organic';
import { useDateRange } from '@/components/DateRangeContext'; import { useDateRange } from '@/components/DateRangeContext';
const BPS_TENANT_ID = '8bec977a-6032-4759-aa7f-b5042081f450';
type Props = { type Props = {
tenantId?: string | null; tenantId?: string | null;
label?: string; label?: string;
@@ -63,7 +61,7 @@ type GoogleCampaignAgg = {
export default function WindsorAiSection({ export default function WindsorAiSection({
tenantId, tenantId,
label = 'Blue Parrot School', label = 'Cliente',
portfolioWide = false, portfolioWide = false,
dailyRows = [], dailyRows = [],
organicRows = [], organicRows = [],
@@ -76,32 +74,62 @@ export default function WindsorAiSection({
const [showAllCampaigns, setShowAllCampaigns] = useState(false); const [showAllCampaigns, setShowAllCampaigns] = useState(false);
const [activeTab, setActiveTab] = useState<'all' | 'google' | 'organic' | 'matrix'>('all'); const [activeTab, setActiveTab] = useState<'all' | 'google' | 'organic' | 'matrix'>('all');
// Determine if context is Blue Parrot School or Portfolio const contextLabel =
const isBps = (label && label.trim()) ||
tenantId === BPS_TENANT_ID || (portfolioWide ? 'Portafolio' : 'Cliente');
label?.toLowerCase().includes('blue parrot') ||
label?.toLowerCase().includes('bps') ||
(!tenantId && !portfolioWide);
// Filter dailyRows for target tenant (prefer BPS rows if in portfolio view) const findIntegration = (provider: string) => {
const rows = (integrations || []).filter((i: any) => i?.provider === provider);
return (
rows.find((i: any) => i?.status === 'connected') ||
rows[0] ||
null
);
};
const gads = findIntegration('google_ads');
const meta = findIntegration('meta_ads');
const ga4 = findIntegration('ga4');
const gsc = findIntegration('search_console');
const clientify = findIntegration('clientify');
const gscSite = String(
gsc?.external_account_id ||
gsc?.meta?.site_url ||
gsc?.label ||
''
)
.replace(/^sc-domain:/i, '')
.replace(/^https?:\/\//i, '')
.replace(/\/$/, '');
const ga4Id = String(
ga4?.meta?.measurement_id ||
ga4?.external_account_id ||
ga4?.label ||
''
);
const gadsId = String(gads?.external_account_id || gads?.label || '')
.replace(/\D/g, '')
.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3');
// Filter dailyRows for the active context (never pin to BPS when another tenant is selected)
const targetDailyRows = useMemo(() => { const targetDailyRows = useMemo(() => {
if (!dailyRows.length) return []; if (!dailyRows.length) return [];
if (tenantId) { if (tenantId) {
return dailyRows.filter((r) => !r.tenant_id || r.tenant_id === tenantId); return dailyRows.filter((r) => !r.tenant_id || r.tenant_id === tenantId);
} }
// In portfolio wide view, focus on BPS rows for this Windsor component return dailyRows;
const bpsRows = dailyRows.filter((r) => r.tenant_id === BPS_TENANT_ID);
return bpsRows.length > 0 ? bpsRows : dailyRows;
}, [dailyRows, tenantId]); }, [dailyRows, tenantId]);
// Filter organicRows for target tenant // Filter organicRows for the active context
const targetOrganicRows = useMemo(() => { const targetOrganicRows = useMemo(() => {
if (!organicRows.length) return []; if (!organicRows.length) return [];
if (tenantId) { if (tenantId) {
return organicRows.filter((r) => !r.tenant_id || r.tenant_id === tenantId); return organicRows.filter((r) => !r.tenant_id || r.tenant_id === tenantId);
} }
const bpsRows = organicRows.filter((r) => r.tenant_id === BPS_TENANT_ID); return organicRows;
return bpsRows.length > 0 ? bpsRows : organicRows;
}, [organicRows, tenantId]); }, [organicRows, tenantId]);
// Apply Date Filtering based on viewMode ('period' vs 'all') // Apply Date Filtering based on viewMode ('period' vs 'all')
@@ -344,42 +372,37 @@ export default function WindsorAiSection({
]; ];
}, [metaData, googleData, organicData, crmLeads, blendedTotals]); }, [metaData, googleData, organicData, crmLeads, blendedTotals]);
// Connectors Status List for blueparrotschool.com // Connectors for the active tenant (from integrations), not a hardcoded BPS list
const connectors = [ const connectors = [
{ {
name: 'Google Ads', name: 'Google Ads',
id: '106-444-1284', id: gadsId || gads?.label || '—',
status: 'connected', status: gads?.status === 'connected' ? 'connected' : 'disconnected',
type: 'Search & PMax', type: 'Search & PMax',
lastSync: 'Normalizado',
}, },
{ {
name: 'Meta Ads', name: 'Meta Ads',
id: 'act_414283212550847', id: meta?.external_account_id || meta?.label || '—',
status: 'connected', status: meta?.status === 'connected' ? 'connected' : 'disconnected',
type: 'Instagram & Facebook Ads', type: 'Instagram & Facebook Ads',
lastSync: 'Normalizado',
}, },
{ {
name: 'Google Analytics 4', name: 'Google Analytics 4',
id: 'G-8CMQFCK82C (469229613)', id: ga4Id || '—',
status: 'connected', status: ga4?.status === 'connected' ? 'connected' : 'disconnected',
type: 'Sesiones & Eventos Web', type: 'Sesiones & Eventos Web',
lastSync: 'Sincronizado',
}, },
{ {
name: 'Search Console', name: 'Search Console',
id: 'blueparrotschool.com', id: gscSite || '—',
status: 'connected', status: gsc?.status === 'connected' ? 'connected' : 'disconnected',
type: 'Tráfico & Clics Orgánicos', type: 'Tráfico & Clics Orgánicos',
lastSync: 'Sincronizado',
}, },
{ {
name: 'Clientify CRM', name: 'Clientify CRM',
id: 'bps (Webhook & API)', id: clientify?.label || clientify?.external_account_id || '—',
status: 'connected', status: clientify?.status === 'connected' ? 'connected' : 'disconnected',
type: 'Leads & Closed-Loop Won', type: 'Leads & Closed-Loop Won',
lastSync: 'En tiempo real',
}, },
]; ];
@@ -403,7 +426,7 @@ export default function WindsorAiSection({
</div> </div>
<h3 className="text-lg font-bold text-gray-900 mt-2 flex items-center gap-2"> <h3 className="text-lg font-bold text-gray-900 mt-2 flex items-center gap-2">
Datos Windsor.ai · blueparrotschool.com Atribución omnicanal · {contextLabel}
</h3> </h3>
<p className="text-xs text-gray-600 mt-0.5"> <p className="text-xs text-gray-600 mt-0.5">
Atribución omnicanal y métricas agregadas multi-red (Google Ads, Meta Ads, GA4, Search Atribución omnicanal y métricas agregadas multi-red (Google Ads, Meta Ads, GA4, Search
@@ -442,7 +465,7 @@ export default function WindsorAiSection({
<div className="bg-slate-50 border border-slate-200/80 rounded-xl p-3"> <div className="bg-slate-50 border border-slate-200/80 rounded-xl p-3">
<p className="text-[11px] font-semibold uppercase tracking-wider text-slate-500 mb-2 flex items-center gap-1.5"> <p className="text-[11px] font-semibold uppercase tracking-wider text-slate-500 mb-2 flex items-center gap-1.5">
<Database className="w-3.5 h-3.5 text-indigo-600" /> <Database className="w-3.5 h-3.5 text-indigo-600" />
Conectores Windsor.ai activos para blueparrotschool.com: Conectores activos para {contextLabel}:
</p> </p>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{connectors.map((c) => ( {connectors.map((c) => (
@@ -450,7 +473,11 @@ export default function WindsorAiSection({
key={c.name} key={c.name}
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-white border border-slate-200 shadow-xs text-xs" className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-white border border-slate-200 shadow-xs text-xs"
> >
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 shrink-0" /> <CheckCircle2
className={`w-3.5 h-3.5 shrink-0 ${
c.status === 'connected' ? 'text-emerald-600' : 'text-gray-300'
}`}
/>
<span className="font-semibold text-gray-800">{c.name}</span> <span className="font-semibold text-gray-800">{c.name}</span>
<span className="text-[11px] text-gray-500 font-mono">({c.id})</span> <span className="text-[11px] text-gray-500 font-mono">({c.id})</span>
</div> </div>
@@ -586,7 +613,8 @@ export default function WindsorAiSection({
Campañas Google Ads · Búsqueda & Performance Max (Windsor) Campañas Google Ads · Búsqueda & Performance Max (Windsor)
</h4> </h4>
<p className="text-xs text-gray-600 mt-0.5"> <p className="text-xs text-gray-600 mt-0.5">
Datos de Google Ads (`ID: 106-444-1284`) no desglosados en el cruce agregado. Normalizados con micro-unidades y atribución Windsor. Datos de Google Ads
{gadsId ? ` (ID: ${gadsId})` : ''} normalizados en el warehouse.
</p> </p>
</div> </div>
{viewMode === 'period' && googleData.campaigns.length === 0 && ( {viewMode === 'period' && googleData.campaigns.length === 0 && (
@@ -602,7 +630,7 @@ export default function WindsorAiSection({
No hay actividad de Google Ads registrada dentro del periodo seleccionado ({rangeLabel}). No hay actividad de Google Ads registrada dentro del periodo seleccionado ({rangeLabel}).
</p> </p>
<p className="text-[11px] text-amber-700 mt-1"> <p className="text-[11px] text-amber-700 mt-1">
En el warehouse de 4Ventures constan 334 registros históricos de Google Ads para blueparrotschool.com (abril–septiembre 2026). No hay filas de Google Ads para {contextLabel} en el warehouse con el filtro actual.
</p> </p>
<button <button
onClick={() => setViewMode('all')} onClick={() => setViewMode('all')}
@@ -725,7 +753,8 @@ export default function WindsorAiSection({
Tráfico Web & Rendimiento Orgánico (GA4 & Google Search Console) Tráfico Web & Rendimiento Orgánico (GA4 & Google Search Console)
</h4> </h4>
<p className="text-xs text-gray-600 mt-0.5"> <p className="text-xs text-gray-600 mt-0.5">
Posicionamiento orgánico y analítica de sesiones web registradas para blueparrotschool.com. Posicionamiento orgánico y analítica de sesiones web registradas para{' '}
{gscSite || contextLabel}.
</p> </p>
</div> </div>
</div> </div>
@@ -739,7 +768,7 @@ export default function WindsorAiSection({
Google Search Console (SEO) Google Search Console (SEO)
</span> </span>
<span className="text-[11px] font-mono text-emerald-700 bg-emerald-100/60 px-2 py-0.5 rounded"> <span className="text-[11px] font-mono text-emerald-700 bg-emerald-100/60 px-2 py-0.5 rounded">
blueparrotschool.com {gscSite || 'Sin GSC'}
</span> </span>
</div> </div>
@@ -783,7 +812,7 @@ export default function WindsorAiSection({
Google Analytics 4 (Web Analytics) Google Analytics 4 (Web Analytics)
</span> </span>
<span className="text-[11px] font-mono text-blue-700 bg-blue-100/60 px-2 py-0.5 rounded"> <span className="text-[11px] font-mono text-blue-700 bg-blue-100/60 px-2 py-0.5 rounded">
G-8CMQFCK82C {ga4Id || 'Sin GA4'}
</span> </span>
</div> </div>