Unify metrics into a single dashboard scorecard.
Fold CRM and organic KPIs into Métricas and hide the duplicate Windsor strip when embedded. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
||||
TrendingUp,
|
||||
Users,
|
||||
MousePointerClick,
|
||||
Globe,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
AdDailyRow,
|
||||
@@ -300,6 +302,29 @@ export default function MetricsDashboard({
|
||||
hasCohort && periodTotals.revenue <= 0 && cohortFromLeads;
|
||||
const showWow = rangeDaySpan(since, until) <= 7;
|
||||
|
||||
const organicPeriod = useMemo(() => {
|
||||
const rows = organicRows.filter((r) => {
|
||||
const d = (r.day || r.date || '').slice(0, 10);
|
||||
return d >= since && d <= until;
|
||||
});
|
||||
const gscClicks = rows.reduce((a, r) => a + (Number(r.gsc_clicks) || 0), 0);
|
||||
const ga4Sessions = rows.reduce(
|
||||
(a, r) => a + (Number(r.ga4_sessions) || 0),
|
||||
0
|
||||
);
|
||||
const ga4Conversions = rows.reduce(
|
||||
(a, r) => a + (Number(r.ga4_conversions) || 0),
|
||||
0
|
||||
);
|
||||
const denom = periodTotals.leads + ga4Conversions;
|
||||
return {
|
||||
gscClicks,
|
||||
ga4Sessions,
|
||||
ga4Conversions,
|
||||
organicShare: denom > 0 ? (ga4Conversions / denom) * 100 : 0,
|
||||
};
|
||||
}, [organicRows, since, until, periodTotals.leads]);
|
||||
|
||||
const cards: Card[] = [
|
||||
{
|
||||
key: 'spend',
|
||||
@@ -311,7 +336,7 @@ export default function MetricsDashboard({
|
||||
{
|
||||
key: 'leads',
|
||||
label: 'Leads plataforma',
|
||||
hint: 'Conversiones reportadas por ads',
|
||||
hint: 'Conversiones reportadas por ads (no CRM)',
|
||||
format: formatNum,
|
||||
icon: <Users className="w-4 h-4 text-blue-600" />,
|
||||
},
|
||||
@@ -424,41 +449,6 @@ export default function MetricsDashboard({
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ label: 'Inversión periodo', value: formatEur(periodTotals.spend) },
|
||||
{
|
||||
label: 'Leads plataforma',
|
||||
value: formatNum(periodTotals.leads),
|
||||
},
|
||||
{
|
||||
label: 'Leads CRM',
|
||||
value: hasCrm ? formatNum(crmPeriodTotal) : '—',
|
||||
},
|
||||
{
|
||||
label: 'CPL CRM (paid)',
|
||||
value:
|
||||
hasCrm && crmPeriodTotal > 0
|
||||
? `${formatNum(periodTotals.spend / crmPeriodTotal)} €`
|
||||
: periodTotals.cpl
|
||||
? `${formatNum(periodTotals.cpl)} €`
|
||||
: '—',
|
||||
},
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="rounded-xl border border-brand-200 bg-brand-50 px-4 py-3 shadow-sm"
|
||||
>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-brand-700">
|
||||
{s.label}
|
||||
</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
||||
{s.value}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{cards.map((c) => {
|
||||
const emptyCohort = Boolean(c.cohort) && !hasCohort;
|
||||
@@ -493,6 +483,81 @@ export default function MetricsDashboard({
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600">
|
||||
Leads CRM
|
||||
</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
||||
{hasCrm ? formatNum(crmPeriodTotal) : '—'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Altas Clientify en el periodo (no es el mismo número que ads)
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600">
|
||||
CPL CRM
|
||||
</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
||||
{hasCrm && crmPeriodTotal > 0
|
||||
? `${formatNum(periodTotals.spend / crmPeriodTotal)} €`
|
||||
: '—'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Inversión ads ÷ leads CRM (distinto del CPL ads)
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600">
|
||||
Gap plat vs CRM
|
||||
</p>
|
||||
<p
|
||||
className={`text-xl font-bold mt-1 tabular-nums ${
|
||||
hasCrm ? gapClass(crmPeriodTotal - periodTotals.leads) : 'text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{hasCrm
|
||||
? `${crmPeriodTotal - periodTotals.leads > 0 ? '+' : ''}${formatNum(crmPeriodTotal - periodTotals.leads)}`
|
||||
: '—'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
CRM − leads plataforma · detalle por canal abajo
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600 inline-flex items-center gap-1">
|
||||
<Search className="w-3.5 h-3.5" /> Clics SEO
|
||||
</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
||||
{formatNum(organicPeriod.gscClicks)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Search Console · mismo periodo</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600 inline-flex items-center gap-1">
|
||||
<Globe className="w-3.5 h-3.5" /> Sesiones web
|
||||
</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
||||
{formatNum(organicPeriod.ga4Sessions)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
GA4 · {formatNum(organicPeriod.ga4Conversions)} conv. web
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600">
|
||||
Cuota orgánica
|
||||
</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
||||
{formatPct(organicPeriod.organicShare)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Conv. GA4 / (leads ads + conv. web)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="bg-white border border-gray-200 rounded-xl p-5 space-y-4 shadow-sm">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-2">
|
||||
<div>
|
||||
@@ -739,8 +804,8 @@ export default function MetricsDashboard({
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Windsor.ai Data — follows Contexto tenant */}
|
||||
<WindsorAiSection
|
||||
embedded
|
||||
tenantId={tenantId}
|
||||
label={label}
|
||||
portfolioWide={portfolioWide}
|
||||
|
||||
Reference in New Issue
Block a user