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:
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user