From fb313fc38f2e0028ec97698b9223d1f5798ed9ff Mon Sep 17 00:00:00 2001 From: 4Ventures <4ventures@MacBook-Air-de-4Ventures.local> Date: Wed, 23 Sep 2026 12:38:45 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20alinear=20KPIs=20con=20Ad=20Tracker=20po?= =?UTF-8?q?r=20d=C3=ADas=20calendario?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KPIs filtraba por inicio de semana (omitía días de Sep en la semana del 31-ago); ahora Mes/7d/30d suman ad_daily día a día como Ad Tracker. Co-authored-by: Cursor --- src/app/dashboard/kpis/page.tsx | 1 + src/components/MetricsDashboard.tsx | 94 ++++++++++++++--------------- src/lib/metrics.ts | 81 +++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 50 deletions(-) diff --git a/src/app/dashboard/kpis/page.tsx b/src/app/dashboard/kpis/page.tsx index 1d0fb43..4a77f49 100644 --- a/src/app/dashboard/kpis/page.tsx +++ b/src/app/dashboard/kpis/page.tsx @@ -87,6 +87,7 @@ export default async function KpisPage() { sparse={sparse} weeklyRows={weekly} channelPoints={channelSeries.points} + dailyRows={daily} /> ); } diff --git a/src/components/MetricsDashboard.tsx b/src/components/MetricsDashboard.tsx index ce12324..14c1c33 100644 --- a/src/components/MetricsDashboard.tsx +++ b/src/components/MetricsDashboard.tsx @@ -15,14 +15,20 @@ import { CalendarRange, } from 'lucide-react'; import { + AdDailyRow, ChannelWeekPoint, Delta, MetricTotals, formatEur, formatNum, formatPct, + channelTotalsFromDaily, + filterDailyByDayRange, + metricTotalsFromDaily, + resolveKpiDayRange, sumWeekly, KpiWeeklyRow, + KpiRangeKey, } from '@/lib/metrics'; type Card = { @@ -34,7 +40,7 @@ type Card = { invert?: boolean; }; -type RangeKey = '7d' | '30d' | 'month' | 'all'; +type RangeKey = KpiRangeKey; function DeltaTriple({ delta, invert }: { delta: Delta; invert?: boolean }) { const cells: { label: string; v: number | null }[] = [ @@ -82,29 +88,6 @@ function DeltaTriple({ delta, invert }: { delta: Delta; invert?: boolean }) { ); } -function filterWeeklyByRange( - rows: KpiWeeklyRow[], - range: RangeKey -): KpiWeeklyRow[] { - if (range === 'all') return rows; - const now = new Date(); - let from: Date; - if (range === '7d') { - from = new Date(now); - from.setUTCDate(from.getUTCDate() - 7); - } else if (range === '30d') { - from = new Date(now); - from.setUTCDate(from.getUTCDate() - 30); - } else { - from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); - } - return rows.filter((r) => { - if (!r.week) return false; - const d = new Date(r.week); - return !Number.isNaN(d.getTime()) && d >= from; - }); -} - export default function MetricsDashboard({ label, portfolioWide, @@ -115,6 +98,7 @@ export default function MetricsDashboard({ sparse, weeklyRows = [], channelPoints = [], + dailyRows = [], }: { label: string; portfolioWide: boolean; @@ -130,19 +114,35 @@ export default function MetricsDashboard({ sparse: boolean; weeklyRows?: KpiWeeklyRow[]; channelPoints?: ChannelWeekPoint[]; + /** Raw ad_daily — period totals/matrix use calendar days (same as Ad Tracker). */ + dailyRows?: AdDailyRow[]; }) { const [range, setRange] = useState('month'); - const ranged = useMemo( - () => filterWeeklyByRange(weeklyRows, range), - [weeklyRows, range] - ); - const periodTotals = useMemo( - () => (ranged.length ? sumWeekly(ranged) : month), - [ranged, month] - ); + const dayWindow = useMemo(() => resolveKpiDayRange(range), [range]); + + const rangedDaily = useMemo(() => { + if (!dailyRows.length) return []; + if (!dayWindow) return dailyRows; + return filterDailyByDayRange(dailyRows, dayWindow.since, dayWindow.until); + }, [dailyRows, dayWindow]); + + const periodTotals = useMemo(() => { + if (rangedDaily.length) return metricTotalsFromDaily(rangedDaily); + // Fallback when no ad_daily: approximate from weekly (legacy) + if (!dayWindow) return sumWeekly(weeklyRows); + const from = dayWindow.since; + const until = dayWindow.until; + const ranged = weeklyRows.filter((r) => { + if (!r.week) return false; + const w = r.week.slice(0, 10); + return w <= until && w >= from; + }); + return ranged.length ? sumWeekly(ranged) : month; + }, [rangedDaily, weeklyRows, dayWindow, month]); const channelRows = useMemo(() => { + if (rangedDaily.length) return channelTotalsFromDaily(rangedDaily); const map = new Map< string, { spend: number; leads: number; deals: number; revenue: number } @@ -151,19 +151,9 @@ export default function MetricsDashboard({ range === 'all' ? channelPoints : channelPoints.filter((p) => { - const d = new Date(p.week); - if (Number.isNaN(d.getTime())) return false; - if (range === 'month') { - const now = new Date(); - return ( - d.getUTCFullYear() === now.getUTCFullYear() && - d.getUTCMonth() === now.getUTCMonth() - ); - } - const now = new Date(); - const from = new Date(now); - from.setUTCDate(from.getUTCDate() - (range === '7d' ? 7 : 30)); - return d >= from; + const d = p.week; + if (!d || !dayWindow) return true; + return d >= dayWindow.since && d <= dayWindow.until; }); for (const p of src) { const cur = map.get(p.channel) || { @@ -186,7 +176,11 @@ export default function MetricsDashboard({ roas: v.spend > 0 ? v.revenue / v.spend : 0, })) .sort((a, b) => b.spend - a.spend); - }, [channelPoints, range]); + }, [rangedDaily, channelPoints, range, dayWindow]); + + const rangeHint = dayWindow + ? `${dayWindow.since} → ${dayWindow.until}` + : 'todo el histórico'; const cards: Card[] = [ { @@ -247,7 +241,7 @@ export default function MetricsDashboard({ {portfolioWide ? ' · portafolio' : ' · zoom cliente'} - . Paid unificado ahora; CRM/GEO en siguientes fases. + . Periodo {rangeHint} (mismos días calendario que Ad Tracker).

@@ -348,9 +342,9 @@ export default function MetricsDashboard({
{channelRows.length === 0 ? (

- Sin series de canal en el periodo. Cuando Windsor cargue - Los canales con inversión en ads (Meta, Google) aparecen cuando hay - filas en ad_daily o kpi_weekly para este cliente. + Sin series de canal en el periodo. Los canales Meta/Google aparecen + cuando hay filas en ad_daily para este cliente (elige uno en + Contexto).

) : (
diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index e1f22a2..e57d561 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -312,3 +312,84 @@ export function resolveWeeklyRows( } export { isoDate, weekStart }; + +/** Same calendar windows as Ad Tracker (inclusive day bounds, UTC). */ +export type KpiRangeKey = '7d' | '30d' | 'month' | 'all'; + +export function resolveKpiDayRange( + range: KpiRangeKey, + now = new Date() +): { since: string; until: string } | null { + const today = new Date( + Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) + ); + const until = isoDate(today); + if (range === 'all') return null; + if (range === 'month') { + const since = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), 1)); + return { since: isoDate(since), until }; + } + const since = new Date(today); + // Inclusive windows: 7d = today-6..today, 30d = today-29..today + since.setUTCDate(since.getUTCDate() - (range === '7d' ? 6 : 29)); + return { since: isoDate(since), until }; +} + +export function filterDailyByDayRange( + rows: AdDailyRow[], + since: string, + until: string +): AdDailyRow[] { + return rows.filter((r) => { + const d = (r.date || '').slice(0, 10); + return d >= since && d <= until; + }); +} + +/** Per-channel paid totals from ad_daily (calendar days). */ +export function channelTotalsFromDaily(rows: AdDailyRow[]): { + channel: string; + spend: number; + leads: number; + deals: number; + revenue: number; + cpl: number; + roas: number; +}[] { + const map = new Map(); + for (const r of rows) { + const channel = (r.platform || 'otros').toLowerCase(); + const cur = map.get(channel) || { spend: 0, leads: 0 }; + cur.spend += n(r.spend); + cur.leads += n(r.platform_leads ?? r.conversions); + map.set(channel, cur); + } + return [...map.entries()] + .map(([channel, v]) => ({ + channel, + spend: v.spend, + leads: v.leads, + deals: 0, + revenue: 0, + cpl: v.leads > 0 ? v.spend / v.leads : 0, + roas: 0, + })) + .sort((a, b) => b.spend - a.spend); +} + +export function metricTotalsFromDaily(rows: AdDailyRow[]): MetricTotals { + const spend = rows.reduce((a, r) => a + n(r.spend), 0); + const leads = rows.reduce( + (a, r) => a + n(r.platform_leads ?? r.conversions), + 0 + ); + return { + spend, + leads, + deals: 0, + revenue: 0, + cac: 0, + roas: 0, + cpl: leads > 0 ? spend / leads : 0, + }; +}