feat: periodo global, métricas negocio, seguimiento notas, Drive/Social/Orgánico

Selector de fechas en header (con Personalizado) compartido en el dashboard; Métricas con Ventas/CAC/ROAS CRM, cruce canal y gráficas diarias; Seguimiento solo notas + sync Superhuman; Activos con navegación Drive; Social con métricas IG/FB; Orgánico con prioridades SEO/GEO expertas.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 15:45:30 +02:00
parent eb1622c88c
commit 3425364b07
15 changed files with 1067 additions and 1011 deletions

View File

@@ -1,8 +1,7 @@
'use client';
import React, { useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import {
CalendarRange,
Users,
UserCheck,
Percent,
@@ -14,7 +13,6 @@ import {
AdDailyRow,
CrmChannelStats,
CrmLeadRow,
KpiRangeKey,
attributionCross,
buildCrmChannelStats,
buildCrmFunnel,
@@ -23,11 +21,9 @@ import {
filterLeadsByDayRange,
formatEur,
formatNum,
resolveKpiDayRange,
} from '@/lib/metrics';
import { PageHeader } from '@/components/ui';
type RangeKey = KpiRangeKey;
import { useDateRange } from '@/components/DateRangeContext';
function rateClass(rate: number): string {
if (rate >= 8) return 'text-emerald-400';
@@ -47,21 +43,17 @@ export default function CrmDashboard({
crmLeads?: CrmLeadRow[];
dailyRows?: AdDailyRow[];
}) {
const [range, setRange] = useState<RangeKey>('all');
const dayWindow = useMemo(() => resolveKpiDayRange(range), [range]);
const { since, until, label: rangeHint } = useDateRange();
const rangedLeads = useMemo(() => {
if (!crmLeads.length) return [];
if (!dayWindow) return crmLeads;
return filterLeadsByDayRange(crmLeads, dayWindow.since, dayWindow.until);
}, [crmLeads, dayWindow]);
return filterLeadsByDayRange(crmLeads, since, until);
}, [crmLeads, since, until]);
const rangedDaily = useMemo(() => {
if (!dailyRows.length) return [];
if (!dayWindow) return dailyRows;
return filterDailyByDayRange(dailyRows, dayWindow.since, dayWindow.until);
}, [dailyRows, dayWindow]);
return filterDailyByDayRange(dailyRows, since, until);
}, [dailyRows, since, until]);
const funnel = useMemo(() => buildCrmFunnel(rangedLeads), [rangedLeads]);
const statusRows = useMemo(
@@ -83,10 +75,6 @@ export default function CrmDashboard({
return m;
}, [attribution]);
const rangeHint = dayWindow
? `${dayWindow.since} → ${dayWindow.until}`
: 'todo el histórico CRM';
const paidChannels = channelRows.filter((r) =>
['meta', 'google'].includes(r.channel)
);
@@ -99,35 +87,7 @@ export default function CrmDashboard({
<PageHeader
eyebrow="Rendimiento"
title="CRM"
description={`${label}${portfolioWide ? ' · portafolio' : ''}. Funnel y % a cliente por canal · ${rangeHint}.`}
actions={
<div className="flex items-center gap-2">
<CalendarRange className="w-4 h-4 text-gray-500" />
<div className="flex gap-1 rounded-lg border border-gray-200 p-0.5 bg-gray-50">
{(
[
['7d', '7d'],
['30d', '30d'],
['month', 'Mes'],
['all', 'Todo'],
] as [RangeKey, string][]
).map(([k, lbl]) => (
<button
key={k}
type="button"
onClick={() => setRange(k)}
className={`px-2.5 py-1 text-xs font-semibold rounded-md ${
range === k
? 'bg-brand-500 text-white shadow-sm'
: 'text-gray-600 hover:text-gray-900 hover:bg-white'
}`}
>
{lbl}
</button>
))}
</div>
</div>
}
description={`${label}${portfolioWide ? ' · portafolio' : ''}. Funnel y % a cliente por canal · periodo ${rangeHint} (header).`}
/>
{!crmLeads.length ? (