feat: rango de fechas personalizado y datos hasta ayer (salvo Hoy)
Selector libre en Ad Tracker/Seguimiento; presets y KPIs cierran en ayer. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AD_TRACKER_PRESETS,
|
|
||||||
RangePreset,
|
RangePreset,
|
||||||
normalizePlatform,
|
normalizePlatform,
|
||||||
resolveAdRange,
|
resolveAdRange,
|
||||||
@@ -14,6 +13,7 @@ import {
|
|||||||
CreativeZoomModal,
|
CreativeZoomModal,
|
||||||
} from '@/components/AdCreativesGallery';
|
} from '@/components/AdCreativesGallery';
|
||||||
import { PageHeader } from '@/components/ui';
|
import { PageHeader } from '@/components/ui';
|
||||||
|
import DateRangePicker from '@/components/DateRangePicker';
|
||||||
|
|
||||||
type PlatformKpis = {
|
type PlatformKpis = {
|
||||||
spend: number;
|
spend: number;
|
||||||
@@ -318,6 +318,8 @@ export default function AdTrackerClient({
|
|||||||
rows: AdDailyRow[];
|
rows: AdDailyRow[];
|
||||||
}) {
|
}) {
|
||||||
const [preset, setPreset] = useState<RangePreset>('last_7d');
|
const [preset, setPreset] = useState<RangePreset>('last_7d');
|
||||||
|
const [customSince, setCustomSince] = useState('');
|
||||||
|
const [customUntil, setCustomUntil] = useState('');
|
||||||
const [tab, setTab] = useState<Tab>('resumen');
|
const [tab, setTab] = useState<Tab>('resumen');
|
||||||
const [chartMode, setChartMode] = useState<ChartMode>('combined');
|
const [chartMode, setChartMode] = useState<ChartMode>('combined');
|
||||||
const [creatives, setCreatives] = useState<AdCreativeCard[]>([]);
|
const [creatives, setCreatives] = useState<AdCreativeCard[]>([]);
|
||||||
@@ -325,7 +327,10 @@ export default function AdTrackerClient({
|
|||||||
const [creativesWarning, setCreativesWarning] = useState<string | null>(null);
|
const [creativesWarning, setCreativesWarning] = useState<string | null>(null);
|
||||||
const [zoomAd, setZoomAd] = useState<AdCreativeCard | null>(null);
|
const [zoomAd, setZoomAd] = useState<AdCreativeCard | null>(null);
|
||||||
|
|
||||||
const range = useMemo(() => resolveAdRange(preset), [preset]);
|
const range = useMemo(
|
||||||
|
() => resolveAdRange(preset, { since: customSince, until: customUntil }),
|
||||||
|
[preset, customSince, customUntil]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -415,30 +420,23 @@ export default function AdTrackerClient({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Period presets */}
|
{/* Period presets */}
|
||||||
<div>
|
<DateRangePicker
|
||||||
<p className="text-[10px] uppercase tracking-wider text-gray-500 mb-2">
|
preset={preset}
|
||||||
Periodo
|
customSince={customSince || range.since}
|
||||||
</p>
|
customUntil={customUntil || range.until}
|
||||||
<div className="flex flex-wrap gap-1.5">
|
onPreset={(p) => {
|
||||||
{AD_TRACKER_PRESETS.map((p) => (
|
setPreset(p);
|
||||||
<button
|
if (p !== 'custom') {
|
||||||
key={p.id}
|
const r = resolveAdRange(p);
|
||||||
type="button"
|
setCustomSince(r.since);
|
||||||
onClick={() => setPreset(p.id)}
|
setCustomUntil(r.until);
|
||||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
|
}
|
||||||
preset === p.id
|
}}
|
||||||
? 'bg-brand-500 text-white'
|
onCustomChange={(since, until) => {
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-50'
|
setCustomSince(since);
|
||||||
}`}
|
setCustomUntil(until);
|
||||||
>
|
}}
|
||||||
{p.label}
|
/>
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<span className="px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-200 text-gray-600">
|
|
||||||
{range.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
|||||||
154
src/components/DateRangePicker.tsx
Normal file
154
src/components/DateRangePicker.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
AD_TRACKER_PRESETS,
|
||||||
|
RangePreset,
|
||||||
|
isoYesterday,
|
||||||
|
resolveAdRange,
|
||||||
|
} from '@/lib/ad-tracker-range';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
preset: RangePreset;
|
||||||
|
customSince: string;
|
||||||
|
customUntil: string;
|
||||||
|
onPreset: (preset: RangePreset) => void;
|
||||||
|
onCustomChange: (since: string, until: string) => void;
|
||||||
|
/** Compact = fewer labels (for sticky headers). */
|
||||||
|
compact?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodo predefinido + rango libre.
|
||||||
|
* El hasta personalizado no puede pasar de ayer (Hoy solo vía botón «Hoy»).
|
||||||
|
*/
|
||||||
|
export default function DateRangePicker({
|
||||||
|
preset,
|
||||||
|
customSince,
|
||||||
|
customUntil,
|
||||||
|
onPreset,
|
||||||
|
onCustomChange,
|
||||||
|
compact = false,
|
||||||
|
}: Props) {
|
||||||
|
const yesterday = useMemo(() => isoYesterday(), []);
|
||||||
|
const range = useMemo(
|
||||||
|
() =>
|
||||||
|
resolveAdRange(preset, {
|
||||||
|
since: customSince,
|
||||||
|
until: customUntil,
|
||||||
|
}),
|
||||||
|
[preset, customSince, customUntil]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [draftSince, setDraftSince] = useState(customSince || range.since);
|
||||||
|
const [draftUntil, setDraftUntil] = useState(customUntil || range.until);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (preset === 'custom') {
|
||||||
|
setDraftSince(customSince || range.since);
|
||||||
|
setDraftUntil(customUntil || range.until);
|
||||||
|
} else {
|
||||||
|
setDraftSince(range.since);
|
||||||
|
setDraftUntil(range.until);
|
||||||
|
}
|
||||||
|
}, [preset, customSince, customUntil, range.since, range.until]);
|
||||||
|
|
||||||
|
function applyCustom() {
|
||||||
|
let s = draftSince || yesterday;
|
||||||
|
let u = draftUntil || yesterday;
|
||||||
|
if (u > yesterday) u = yesterday;
|
||||||
|
if (s > u) s = u;
|
||||||
|
onCustomChange(s, u);
|
||||||
|
onPreset('custom');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{!compact ? (
|
||||||
|
<p className="text-[10px] uppercase tracking-wider text-gray-500">
|
||||||
|
Periodo
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<div className="flex flex-wrap gap-1.5 items-center">
|
||||||
|
{AD_TRACKER_PRESETS.map((p) => (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onPreset(p.id)}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
|
||||||
|
preset === p.id
|
||||||
|
? 'bg-brand-500 text-white'
|
||||||
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{p.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
let until = draftUntil || range.until;
|
||||||
|
let since = draftSince || range.since;
|
||||||
|
if (until > yesterday) until = yesterday;
|
||||||
|
if (since > until) since = until;
|
||||||
|
setDraftSince(since);
|
||||||
|
setDraftUntil(until);
|
||||||
|
onCustomChange(since, until);
|
||||||
|
onPreset('custom');
|
||||||
|
}}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
|
||||||
|
preset === 'custom'
|
||||||
|
? 'bg-brand-500 text-white'
|
||||||
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Personalizado
|
||||||
|
</button>
|
||||||
|
<span className="px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-200 text-gray-600">
|
||||||
|
{range.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{preset === 'custom' ? (
|
||||||
|
<div className="flex flex-wrap items-end gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2.5">
|
||||||
|
<label className="text-xs text-gray-600">
|
||||||
|
Desde
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={draftSince}
|
||||||
|
max={draftUntil || yesterday}
|
||||||
|
onChange={(e) => setDraftSince(e.target.value)}
|
||||||
|
className="mt-1 block rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-sm text-gray-900"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="text-xs text-gray-600">
|
||||||
|
Hasta
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={draftUntil}
|
||||||
|
max={yesterday}
|
||||||
|
min={draftSince || undefined}
|
||||||
|
onChange={(e) => setDraftUntil(e.target.value)}
|
||||||
|
className="mt-1 block rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-sm text-gray-900"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={applyCustom}
|
||||||
|
className="rounded-lg bg-gray-900 px-3 py-1.5 text-xs font-semibold text-white hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
Aplicar
|
||||||
|
</button>
|
||||||
|
<p className="w-full text-[10px] text-gray-500">
|
||||||
|
El rango personalizado llega como máximo a ayer. Usa «Hoy» para
|
||||||
|
incluir el día en curso.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : preset !== 'today' ? (
|
||||||
|
<p className="text-[10px] text-gray-400">
|
||||||
|
Datos hasta ayer · «Hoy» incluye el día en curso
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,8 +25,8 @@ import {
|
|||||||
sumWeekly,
|
sumWeekly,
|
||||||
} from '@/lib/metrics';
|
} from '@/lib/metrics';
|
||||||
import { PageHeader } from '@/components/ui';
|
import { PageHeader } from '@/components/ui';
|
||||||
|
import DateRangePicker from '@/components/DateRangePicker';
|
||||||
import {
|
import {
|
||||||
AD_TRACKER_PRESETS,
|
|
||||||
RangePreset,
|
RangePreset,
|
||||||
resolveAdRange,
|
resolveAdRange,
|
||||||
} from '@/lib/ad-tracker-range';
|
} from '@/lib/ad-tracker-range';
|
||||||
@@ -263,6 +263,8 @@ export default function SeguimientoBoard() {
|
|||||||
const [openId, setOpenId] = useState<string | null>(null);
|
const [openId, setOpenId] = useState<string | null>(null);
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [preset, setPreset] = useState<RangePreset>('last_30d');
|
const [preset, setPreset] = useState<RangePreset>('last_30d');
|
||||||
|
const [customSince, setCustomSince] = useState('');
|
||||||
|
const [customUntil, setCustomUntil] = useState('');
|
||||||
const [noteTitle, setNoteTitle] = useState('');
|
const [noteTitle, setNoteTitle] = useState('');
|
||||||
const [noteBody, setNoteBody] = useState('');
|
const [noteBody, setNoteBody] = useState('');
|
||||||
const [noteBusy, setNoteBusy] = useState(false);
|
const [noteBusy, setNoteBusy] = useState(false);
|
||||||
@@ -288,7 +290,10 @@ export default function SeguimientoBoard() {
|
|||||||
void load();
|
void load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const range = useMemo(() => resolveAdRange(preset), [preset]);
|
const range = useMemo(
|
||||||
|
() => resolveAdRange(preset, { since: customSince, until: customUntil }),
|
||||||
|
[preset, customSince, customUntil]
|
||||||
|
);
|
||||||
|
|
||||||
const rangedWeekly = useMemo(() => {
|
const rangedWeekly = useMemo(() => {
|
||||||
const rows = data?.weekly || [];
|
const rows = data?.weekly || [];
|
||||||
@@ -450,28 +455,28 @@ export default function SeguimientoBoard() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Date selector */}
|
{/* Date selector */}
|
||||||
<section className="rounded-2xl border border-white/[0.06] bg-card/80 p-4 space-y-3">
|
<section className="rounded-2xl border border-gray-200 bg-white p-4 space-y-3">
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||||
<CalendarRange className="w-3.5 h-3.5 text-sky-400" />
|
<CalendarRange className="w-3.5 h-3.5 text-sky-500" />
|
||||||
Periodo
|
Periodo
|
||||||
<span className="text-gray-500">· {range.label}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
{AD_TRACKER_PRESETS.map((p) => (
|
|
||||||
<button
|
|
||||||
key={p.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setPreset(p.id)}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
|
|
||||||
preset === p.id
|
|
||||||
? 'bg-brand-500 text-white'
|
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{p.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
<DateRangePicker
|
||||||
|
preset={preset}
|
||||||
|
customSince={customSince || range.since}
|
||||||
|
customUntil={customUntil || range.until}
|
||||||
|
onPreset={(p) => {
|
||||||
|
setPreset(p);
|
||||||
|
if (p !== 'custom') {
|
||||||
|
const r = resolveAdRange(p);
|
||||||
|
setCustomSince(r.since);
|
||||||
|
setCustomUntil(r.until);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCustomChange={(since, until) => {
|
||||||
|
setCustomSince(since);
|
||||||
|
setCustomUntil(until);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* KPI cards (Métricas-like) */}
|
{/* KPI cards (Métricas-like) */}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/** Date presets for Ad Tracker (Brava-style). No date-fns dependency. */
|
/** Date presets for Ad Tracker. Data ends at yesterday unless preset is «Hoy». */
|
||||||
|
|
||||||
export type RangePreset =
|
export type RangePreset =
|
||||||
| 'today'
|
| 'today'
|
||||||
@@ -34,6 +34,36 @@ function iso(d: Date): string {
|
|||||||
return d.toISOString().slice(0, 10);
|
return d.toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** UTC calendar day for «today». */
|
||||||
|
export function utcToday(now = new Date()): Date {
|
||||||
|
return new Date(
|
||||||
|
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** UTC calendar day for «yesterday». */
|
||||||
|
export function utcYesterday(now = new Date()): Date {
|
||||||
|
const d = utcToday(now);
|
||||||
|
d.setUTCDate(d.getUTCDate() - 1);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isoToday(now = new Date()): string {
|
||||||
|
return iso(utcToday(now));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isoYesterday(now = new Date()): string {
|
||||||
|
return iso(utcYesterday(now));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max date allowed in the picker / default until.
|
||||||
|
* Only «Hoy» may include today; everything else ends at yesterday.
|
||||||
|
*/
|
||||||
|
export function maxDataDate(preset: RangePreset, now = new Date()): string {
|
||||||
|
return preset === 'today' ? isoToday(now) : isoYesterday(now);
|
||||||
|
}
|
||||||
|
|
||||||
function startOfWeekMon(d: Date): Date {
|
function startOfWeekMon(d: Date): Date {
|
||||||
const x = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
const x = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
||||||
const day = x.getUTCDay();
|
const day = x.getUTCDay();
|
||||||
@@ -64,45 +94,65 @@ export function formatRangeLabel(since: string, until: string): string {
|
|||||||
return `${a[2]} ${MONTHS_ES[a[1] - 1]} ${a[0]} - ${b[2]} ${MONTHS_ES[b[1] - 1]} ${b[0]}`;
|
return `${a[2]} ${MONTHS_ES[a[1] - 1]} ${a[0]} - ${b[2]} ${MONTHS_ES[b[1] - 1]} ${b[0]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseIsoDay(s: string): Date | null {
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return null;
|
||||||
|
const d = new Date(s + 'T00:00:00Z');
|
||||||
|
return Number.isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a date range.
|
||||||
|
* All presets except «today» end at yesterday (ads warehouse lag).
|
||||||
|
* Custom ranges are clamped so until ≤ yesterday.
|
||||||
|
*/
|
||||||
export function resolveAdRange(
|
export function resolveAdRange(
|
||||||
preset: RangePreset,
|
preset: RangePreset,
|
||||||
custom?: { since?: string; until?: string }
|
custom?: { since?: string; until?: string },
|
||||||
|
now = new Date()
|
||||||
): DateRange {
|
): DateRange {
|
||||||
const now = new Date();
|
const today = utcToday(now);
|
||||||
const today = new Date(
|
const yesterday = utcYesterday(now);
|
||||||
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
/** Anchor end for non-today presets. */
|
||||||
);
|
const end = yesterday;
|
||||||
|
|
||||||
let since = today;
|
let since = new Date(end);
|
||||||
let until = today;
|
let until = new Date(end);
|
||||||
|
|
||||||
switch (preset) {
|
switch (preset) {
|
||||||
case 'today':
|
case 'today':
|
||||||
|
since = new Date(today);
|
||||||
|
until = new Date(today);
|
||||||
break;
|
break;
|
||||||
case 'yesterday':
|
case 'yesterday':
|
||||||
since = new Date(today);
|
since = new Date(yesterday);
|
||||||
since.setUTCDate(since.getUTCDate() - 1);
|
until = new Date(yesterday);
|
||||||
until = new Date(since);
|
|
||||||
break;
|
break;
|
||||||
case 'last_7d':
|
case 'last_7d':
|
||||||
since = new Date(today);
|
since = new Date(end);
|
||||||
since.setUTCDate(since.getUTCDate() - 6);
|
since.setUTCDate(since.getUTCDate() - 6);
|
||||||
|
until = new Date(end);
|
||||||
break;
|
break;
|
||||||
case 'last_14d':
|
case 'last_14d':
|
||||||
since = new Date(today);
|
since = new Date(end);
|
||||||
since.setUTCDate(since.getUTCDate() - 13);
|
since.setUTCDate(since.getUTCDate() - 13);
|
||||||
|
until = new Date(end);
|
||||||
break;
|
break;
|
||||||
case 'last_28d':
|
case 'last_28d':
|
||||||
since = new Date(today);
|
since = new Date(end);
|
||||||
since.setUTCDate(since.getUTCDate() - 27);
|
since.setUTCDate(since.getUTCDate() - 27);
|
||||||
|
until = new Date(end);
|
||||||
break;
|
break;
|
||||||
case 'last_30d':
|
case 'last_30d':
|
||||||
since = new Date(today);
|
since = new Date(end);
|
||||||
since.setUTCDate(since.getUTCDate() - 29);
|
since.setUTCDate(since.getUTCDate() - 29);
|
||||||
|
until = new Date(end);
|
||||||
break;
|
break;
|
||||||
case 'this_week':
|
case 'this_week':
|
||||||
since = startOfWeekMon(today);
|
since = startOfWeekMon(today);
|
||||||
until = today;
|
until = new Date(end);
|
||||||
|
if (since.getTime() > until.getTime()) {
|
||||||
|
since = new Date(until);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'last_week': {
|
case 'last_week': {
|
||||||
const thisMon = startOfWeekMon(today);
|
const thisMon = startOfWeekMon(today);
|
||||||
@@ -113,7 +163,11 @@ export function resolveAdRange(
|
|||||||
}
|
}
|
||||||
case 'this_month':
|
case 'this_month':
|
||||||
since = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), 1));
|
since = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), 1));
|
||||||
until = today;
|
until = new Date(end);
|
||||||
|
if (since.getTime() > until.getTime()) {
|
||||||
|
// 1st of month = today → no complete days yet; show yesterday of prev month
|
||||||
|
since = new Date(until);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'last_month': {
|
case 'last_month': {
|
||||||
const firstThis = new Date(
|
const firstThis = new Date(
|
||||||
@@ -124,11 +178,18 @@ export function resolveAdRange(
|
|||||||
since = new Date(Date.UTC(until.getUTCFullYear(), until.getUTCMonth(), 1));
|
since = new Date(Date.UTC(until.getUTCFullYear(), until.getUTCMonth(), 1));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'custom':
|
case 'custom': {
|
||||||
since = custom?.since ? new Date(custom.since + 'T00:00:00Z') : since;
|
const maxUntil = yesterday;
|
||||||
until = custom?.until ? new Date(custom.until + 'T00:00:00Z') : until;
|
let s = parseIsoDay(custom?.since || '') || new Date(end);
|
||||||
|
let u = parseIsoDay(custom?.until || '') || new Date(end);
|
||||||
|
// Clamp: never past yesterday for custom
|
||||||
|
if (u.getTime() > maxUntil.getTime()) u = new Date(maxUntil);
|
||||||
|
if (s.getTime() > u.getTime()) s = new Date(u);
|
||||||
|
since = s;
|
||||||
|
until = u;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const s = iso(since);
|
const s = iso(since);
|
||||||
const u = iso(until);
|
const u = iso(until);
|
||||||
|
|||||||
@@ -313,7 +313,8 @@ export function resolveWeeklyRows(
|
|||||||
|
|
||||||
export { isoDate, weekStart };
|
export { isoDate, weekStart };
|
||||||
|
|
||||||
/** Same calendar windows as Ad Tracker (inclusive day bounds, UTC). */
|
/** Same calendar windows as Ad Tracker (inclusive day bounds, UTC).
|
||||||
|
* Ends at yesterday (warehouse lag) — never includes today. */
|
||||||
export type KpiRangeKey = '7d' | '30d' | 'month' | 'all';
|
export type KpiRangeKey = '7d' | '30d' | 'month' | 'all';
|
||||||
|
|
||||||
export function resolveKpiDayRange(
|
export function resolveKpiDayRange(
|
||||||
@@ -323,14 +324,21 @@ export function resolveKpiDayRange(
|
|||||||
const today = new Date(
|
const today = new Date(
|
||||||
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
||||||
);
|
);
|
||||||
const until = isoDate(today);
|
const yesterday = new Date(today);
|
||||||
|
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
||||||
|
const until = isoDate(yesterday);
|
||||||
if (range === 'all') return null;
|
if (range === 'all') return null;
|
||||||
if (range === 'month') {
|
if (range === 'month') {
|
||||||
const since = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), 1));
|
const since = new Date(
|
||||||
|
Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), 1)
|
||||||
|
);
|
||||||
|
if (since.getTime() > yesterday.getTime()) {
|
||||||
|
return { since: until, until };
|
||||||
|
}
|
||||||
return { since: isoDate(since), until };
|
return { since: isoDate(since), until };
|
||||||
}
|
}
|
||||||
const since = new Date(today);
|
const since = new Date(yesterday);
|
||||||
// Inclusive windows: 7d = today-6..today, 30d = today-29..today
|
// Inclusive windows ending yesterday: 7d = yesterday-6..yesterday
|
||||||
since.setUTCDate(since.getUTCDate() - (range === '7d' ? 6 : 29));
|
since.setUTCDate(since.getUTCDate() - (range === '7d' ? 6 : 29));
|
||||||
return { since: isoDate(since), until };
|
return { since: isoDate(since), until };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user