'use client'; import React, { useMemo, useState } from 'react'; import { Flame, UserX, Briefcase, GitCompare, ListTree, } from 'lucide-react'; import { AdDailyRow, CrmChannelStats, CrmDateLens, CrmLeadRow, attributionCross, buildCrmChannelStats, buildCrmFunnel, buildCrmStatusBreakdown, filterDailyByDayRange, filterLeadsByDayRange, formatEur, formatNum, } from '@/lib/metrics'; import { PageHeader, SegmentedControl, Surface } from '@/components/ui'; import { useDateRange } from '@/components/DateRangeContext'; import SyncNowButton from '@/components/SyncNowButton'; import { HelpDetails, HeroBand, HeroStat, SectionHead, ShareBar, StatMini, tableHeadClass, } from '@/components/dash-ui'; function rateClass(rate: number): string { if (rate >= 8) return 'text-emerald-800'; if (rate >= 3) return 'text-amber-800'; if (rate > 0) return 'text-orange-800'; return 'text-gray-500'; } const LENS_HELP: Record = { created: 'Altas: leads cuya fecha de creación en el CRM cae en el periodo del header.', status: 'Conversiones: usa la fecha de cambio de status (→ cliente). Requiere columnas status_changed_at.', snapshot: 'Estado actual: ignora el periodo para el funnel; muestra todos los contactos como están ahora.', }; export default function CrmDashboard({ label, portfolioWide, crmLeads = [], dailyRows = [], }: { label: string; portfolioWide: boolean; crmLeads?: CrmLeadRow[]; dailyRows?: AdDailyRow[]; }) { const { since, until, label: rangeHint } = useDateRange(); const [lens, setLens] = useState('created'); const hasStatusTiming = useMemo( () => crmLeads.some((l) => Boolean(l.status_changed_at || l.updated_at)), [crmLeads] ); const rangedLeads = useMemo(() => { if (!crmLeads.length) return []; const effective: CrmDateLens = lens === 'status' && !hasStatusTiming ? 'created' : lens; return filterLeadsByDayRange(crmLeads, since, until, effective); }, [crmLeads, since, until, lens, hasStatusTiming]); const rangedDaily = useMemo(() => { if (!dailyRows.length) return []; return filterDailyByDayRange(dailyRows, since, until); }, [dailyRows, since, until]); const funnel = useMemo(() => buildCrmFunnel(rangedLeads), [rangedLeads]); const statusRows = useMemo( () => buildCrmStatusBreakdown(rangedLeads), [rangedLeads] ); const channelRows: CrmChannelStats[] = useMemo( () => buildCrmChannelStats(rangedLeads), [rangedLeads] ); const attribution = useMemo( () => attributionCross(rangedDaily, rangedLeads), [rangedDaily, rangedLeads] ); const spendByChannel = useMemo(() => { const m = new Map(); for (const r of attribution) m.set(r.channel, r.spend); return m; }, [attribution]); const paidChannels = channelRows.filter((r) => ['meta', 'google'].includes(r.channel) ); const otherChannels = channelRows.filter( (r) => !['meta', 'google'].includes(r.channel) ); const leadMax = Math.max(1, ...channelRows.map((r) => r.leads)); const statusMax = Math.max(1, ...statusRows.map((r) => r.count)); return (
} />

Lente

{LENS_HELP[lens]}

El % a cliente es del CRM, no de ads. La inversión por canal es solo referencia del periodo.

{lens === 'status' && !hasStatusTiming ? (

Aún no hay fecha de cambio de status en warehouse — filtrando por alta. Ejecuta sql/leads_status_timing.sql{' '} y deja que los webhooks actualicen contactos.

) : null} {!crmLeads.length ? (

Sin leads CRM en este contexto. Importa el histórico o activa el webhook. En portafolio, elige un cliente en Contexto.

) : null}
{funnel.enOportunidad} en oportunidad } /> {formatNum(funnel.leadFrio)} fríos } />
} /> } /> } />
} title="% a cliente por canal" hint="Estado ganado en el CRM. Ads spend del periodo es referencia." /> {!channelRows.length ? (

Sin leads en esta lente/periodo.

) : (
{[...paidChannels, ...otherChannels].map((row) => ( ))}
Canal Leads Clientes % a cliente Inversión ads
{row.channel} {formatNum(row.cliente)} {row.clienteRate.toFixed(1)}% {spendByChannel.get(row.channel) ? formatEur(spendByChannel.get(row.channel)!) : '—'}
)}
} title="Desglose de status" hint="Distribución de contactos en el warehouse con la lente activa" /> {!statusRows.length ? (

Sin datos.

) : (
{statusRows.map((row) => ( ))}
Status N %
{row.status} {row.pct.toFixed(1)}%
)}
); }