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:
4Ventures
2026-09-24 10:24:25 +02:00
parent 068faa3d25
commit 8fda2996b6
5 changed files with 299 additions and 73 deletions

View File

@@ -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 =
| 'today'
@@ -34,6 +34,36 @@ function iso(d: Date): string {
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 {
const x = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
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]}`;
}
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(
preset: RangePreset,
custom?: { since?: string; until?: string }
custom?: { since?: string; until?: string },
now = new Date()
): DateRange {
const now = new Date();
const today = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
);
const today = utcToday(now);
const yesterday = utcYesterday(now);
/** Anchor end for non-today presets. */
const end = yesterday;
let since = today;
let until = today;
let since = new Date(end);
let until = new Date(end);
switch (preset) {
case 'today':
since = new Date(today);
until = new Date(today);
break;
case 'yesterday':
since = new Date(today);
since.setUTCDate(since.getUTCDate() - 1);
until = new Date(since);
since = new Date(yesterday);
until = new Date(yesterday);
break;
case 'last_7d':
since = new Date(today);
since = new Date(end);
since.setUTCDate(since.getUTCDate() - 6);
until = new Date(end);
break;
case 'last_14d':
since = new Date(today);
since = new Date(end);
since.setUTCDate(since.getUTCDate() - 13);
until = new Date(end);
break;
case 'last_28d':
since = new Date(today);
since = new Date(end);
since.setUTCDate(since.getUTCDate() - 27);
until = new Date(end);
break;
case 'last_30d':
since = new Date(today);
since = new Date(end);
since.setUTCDate(since.getUTCDate() - 29);
until = new Date(end);
break;
case 'this_week':
since = startOfWeekMon(today);
until = today;
until = new Date(end);
if (since.getTime() > until.getTime()) {
since = new Date(until);
}
break;
case 'last_week': {
const thisMon = startOfWeekMon(today);
@@ -113,7 +163,11 @@ export function resolveAdRange(
}
case 'this_month':
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;
case 'last_month': {
const firstThis = new Date(
@@ -124,10 +178,17 @@ export function resolveAdRange(
since = new Date(Date.UTC(until.getUTCFullYear(), until.getUTCMonth(), 1));
break;
}
case 'custom':
since = custom?.since ? new Date(custom.since + 'T00:00:00Z') : since;
until = custom?.until ? new Date(custom.until + 'T00:00:00Z') : until;
case 'custom': {
const maxUntil = yesterday;
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;
}
}
const s = iso(since);

View File

@@ -313,7 +313,8 @@ export function resolveWeeklyRows(
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 function resolveKpiDayRange(
@@ -323,14 +324,21 @@ export function resolveKpiDayRange(
const today = new Date(
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 === '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 };
}
const since = new Date(today);
// Inclusive windows: 7d = today-6..today, 30d = today-29..today
const since = new Date(yesterday);
// Inclusive windows ending yesterday: 7d = yesterday-6..yesterday
since.setUTCDate(since.getUTCDate() - (range === '7d' ? 6 : 29));
return { since: isoDate(since), until };
}