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>
|
</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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user