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:
@@ -739,7 +739,7 @@ export default function MetricsDashboard({
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Windsor.ai Data for blueparrotschool.com (Directly below Cruce por canal) */}
|
||||
{/* Windsor.ai Data — follows Contexto tenant */}
|
||||
<WindsorAiSection
|
||||
tenantId={tenantId}
|
||||
label={label}
|
||||
|
||||
@@ -35,8 +35,6 @@ import {
|
||||
import { OrganicDailyRow } from '@/lib/organic';
|
||||
import { useDateRange } from '@/components/DateRangeContext';
|
||||
|
||||
const BPS_TENANT_ID = '8bec977a-6032-4759-aa7f-b5042081f450';
|
||||
|
||||
type Props = {
|
||||
tenantId?: string | null;
|
||||
label?: string;
|
||||
@@ -63,7 +61,7 @@ type GoogleCampaignAgg = {
|
||||
|
||||
export default function WindsorAiSection({
|
||||
tenantId,
|
||||
label = 'Blue Parrot School',
|
||||
label = 'Cliente',
|
||||
portfolioWide = false,
|
||||
dailyRows = [],
|
||||
organicRows = [],
|
||||
@@ -76,32 +74,62 @@ export default function WindsorAiSection({
|
||||
const [showAllCampaigns, setShowAllCampaigns] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'all' | 'google' | 'organic' | 'matrix'>('all');
|
||||
|
||||
// Determine if context is Blue Parrot School or Portfolio
|
||||
const isBps =
|
||||
tenantId === BPS_TENANT_ID ||
|
||||
label?.toLowerCase().includes('blue parrot') ||
|
||||
label?.toLowerCase().includes('bps') ||
|
||||
(!tenantId && !portfolioWide);
|
||||
const contextLabel =
|
||||
(label && label.trim()) ||
|
||||
(portfolioWide ? 'Portafolio' : 'Cliente');
|
||||
|
||||
// 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(() => {
|
||||
if (!dailyRows.length) return [];
|
||||
if (tenantId) {
|
||||
return dailyRows.filter((r) => !r.tenant_id || r.tenant_id === tenantId);
|
||||
}
|
||||
// In portfolio wide view, focus on BPS rows for this Windsor component
|
||||
const bpsRows = dailyRows.filter((r) => r.tenant_id === BPS_TENANT_ID);
|
||||
return bpsRows.length > 0 ? bpsRows : dailyRows;
|
||||
return dailyRows;
|
||||
}, [dailyRows, tenantId]);
|
||||
|
||||
// Filter organicRows for target tenant
|
||||
// Filter organicRows for the active context
|
||||
const targetOrganicRows = useMemo(() => {
|
||||
if (!organicRows.length) return [];
|
||||
if (tenantId) {
|
||||
return organicRows.filter((r) => !r.tenant_id || r.tenant_id === tenantId);
|
||||
}
|
||||
const bpsRows = organicRows.filter((r) => r.tenant_id === BPS_TENANT_ID);
|
||||
return bpsRows.length > 0 ? bpsRows : organicRows;
|
||||
return organicRows;
|
||||
}, [organicRows, tenantId]);
|
||||
|
||||
// Apply Date Filtering based on viewMode ('period' vs 'all')
|
||||
@@ -344,42 +372,37 @@ export default function WindsorAiSection({
|
||||
];
|
||||
}, [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 = [
|
||||
{
|
||||
name: 'Google Ads',
|
||||
id: '106-444-1284',
|
||||
status: 'connected',
|
||||
id: gadsId || gads?.label || '—',
|
||||
status: gads?.status === 'connected' ? 'connected' : 'disconnected',
|
||||
type: 'Search & PMax',
|
||||
lastSync: 'Normalizado',
|
||||
},
|
||||
{
|
||||
name: 'Meta Ads',
|
||||
id: 'act_414283212550847',
|
||||
status: 'connected',
|
||||
id: meta?.external_account_id || meta?.label || '—',
|
||||
status: meta?.status === 'connected' ? 'connected' : 'disconnected',
|
||||
type: 'Instagram & Facebook Ads',
|
||||
lastSync: 'Normalizado',
|
||||
},
|
||||
{
|
||||
name: 'Google Analytics 4',
|
||||
id: 'G-8CMQFCK82C (469229613)',
|
||||
status: 'connected',
|
||||
id: ga4Id || '—',
|
||||
status: ga4?.status === 'connected' ? 'connected' : 'disconnected',
|
||||
type: 'Sesiones & Eventos Web',
|
||||
lastSync: 'Sincronizado',
|
||||
},
|
||||
{
|
||||
name: 'Search Console',
|
||||
id: 'blueparrotschool.com',
|
||||
status: 'connected',
|
||||
id: gscSite || '—',
|
||||
status: gsc?.status === 'connected' ? 'connected' : 'disconnected',
|
||||
type: 'Tráfico & Clics Orgánicos',
|
||||
lastSync: 'Sincronizado',
|
||||
},
|
||||
{
|
||||
name: 'Clientify CRM',
|
||||
id: 'bps (Webhook & API)',
|
||||
status: 'connected',
|
||||
id: clientify?.label || clientify?.external_account_id || '—',
|
||||
status: clientify?.status === 'connected' ? 'connected' : 'disconnected',
|
||||
type: 'Leads & Closed-Loop Won',
|
||||
lastSync: 'En tiempo real',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -403,7 +426,7 @@ export default function WindsorAiSection({
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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
|
||||
@@ -442,7 +465,7 @@ export default function WindsorAiSection({
|
||||
<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">
|
||||
<Database className="w-3.5 h-3.5 text-indigo-600" />
|
||||
Conectores Windsor.ai activos para blueparrotschool.com:
|
||||
Conectores activos para {contextLabel}:
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{connectors.map((c) => (
|
||||
@@ -450,7 +473,11 @@ export default function WindsorAiSection({
|
||||
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"
|
||||
>
|
||||
<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="text-[11px] text-gray-500 font-mono">({c.id})</span>
|
||||
</div>
|
||||
@@ -586,7 +613,8 @@ export default function WindsorAiSection({
|
||||
Campañas Google Ads · Búsqueda & Performance Max (Windsor)
|
||||
</h4>
|
||||
<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>
|
||||
</div>
|
||||
{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}).
|
||||
</p>
|
||||
<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>
|
||||
<button
|
||||
onClick={() => setViewMode('all')}
|
||||
@@ -725,7 +753,8 @@ export default function WindsorAiSection({
|
||||
Tráfico Web & Rendimiento Orgánico (GA4 & Google Search Console)
|
||||
</h4>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -739,7 +768,7 @@ export default function WindsorAiSection({
|
||||
Google Search Console (SEO)
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -783,7 +812,7 @@ export default function WindsorAiSection({
|
||||
Google Analytics 4 (Web Analytics)
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user