'use client'; import React, { useMemo } from 'react'; import { Users, UserCheck, Percent, Flame, UserX, Briefcase, } from 'lucide-react'; import { AdDailyRow, CrmChannelStats, CrmLeadRow, attributionCross, buildCrmChannelStats, buildCrmFunnel, buildCrmStatusBreakdown, filterDailyByDayRange, filterLeadsByDayRange, formatEur, formatNum, } from '@/lib/metrics'; import { PageHeader } from '@/components/ui'; import { useDateRange } from '@/components/DateRangeContext'; function rateClass(rate: number): string { if (rate >= 8) return 'text-emerald-400'; if (rate >= 3) return 'text-amber-400'; if (rate > 0) return 'text-orange-400'; return 'text-gray-500'; } export default function CrmDashboard({ label, portfolioWide, crmLeads = [], dailyRows = [], }: { label: string; portfolioWide: boolean; crmLeads?: CrmLeadRow[]; dailyRows?: AdDailyRow[]; }) { const { since, until, label: rangeHint } = useDateRange(); const rangedLeads = useMemo(() => { if (!crmLeads.length) return []; return filterLeadsByDayRange(crmLeads, since, until); }, [crmLeads, since, until]); 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) ); return (
{!crmLeads.length ? (

Sin leads Clientify en este contexto. Importa el histórico o activa el webhook (`?tenant=…`). En portafolio, elige un cliente en Contexto.

) : null} {/* North star */}
{[ { label: 'Leads CRM', value: formatNum(funnel.total), icon: , }, { label: 'Clientes (ganado)', value: formatNum(funnel.cliente), icon: , }, { label: '% a cliente', value: `${funnel.clienteRate.toFixed(1)}%`, icon: , }, { label: 'En oportunidad', value: formatNum(funnel.enOportunidad), icon: , }, { label: 'Lead frío*', value: formatNum(funnel.leadFrio), icon: , }, { label: 'Lead perdido', value: formatNum(funnel.leadPerdido), icon: , }, ].map((s) => (

{s.label}

{s.icon}

{s.value}

))}

*Frío incluye templado / caliente / cold-lead.

{/* % cliente por canal — paid first */}

% a cliente por canal

Estado Clientify = cliente{' '} (ganado). Paid arriba; resto de atribución abajo.

{channelRows.length === 0 ? (

Sin canales en el periodo.

) : (
)}
{/* Funnel statuses */}

Funnel por estado

{statusRows.length === 0 ? (

Sin datos.

) : (
{statusRows.map((row) => ( ))}
Estado Leads % del total
{row.status} {formatNum(row.count)} {row.pct.toFixed(1)}%
)}

La inversión por canal (si hay ads) es solo referencia; el % a cliente es 100% CRM (Clientify).

); } function ChannelTable({ title, rows, spendByChannel, }: { title: string; rows: CrmChannelStats[]; spendByChannel: Map; }) { if (!rows.length) { return (

{title}

Sin filas.

); } return (

{title}

{rows.map((row) => { const spend = spendByChannel.get(row.channel) || 0; return ( ); })}
Canal Leads Clientes % a cliente En oportunidad Frío* Perdido Spend ads
{row.channel} {formatNum(row.leads)} {formatNum(row.cliente)} {row.clienteRate.toFixed(1)}% {formatNum(row.enOportunidad)} {formatNum(row.leadFrio)} {formatNum(row.leadPerdido)} {spend ? formatEur(spend) : '—'}
); }