Selector libre en Ad Tracker/Seguimiento; presets y KPIs cierran en ayer. Co-authored-by: Cursor <cursoragent@cursor.com>
215 lines
5.7 KiB
TypeScript
215 lines
5.7 KiB
TypeScript
/** Date presets for Ad Tracker. Data ends at yesterday unless preset is «Hoy». */
|
|
|
|
export type RangePreset =
|
|
| 'today'
|
|
| 'yesterday'
|
|
| 'last_7d'
|
|
| 'last_14d'
|
|
| 'last_28d'
|
|
| 'last_30d'
|
|
| 'this_week'
|
|
| 'last_week'
|
|
| 'this_month'
|
|
| 'last_month'
|
|
| 'custom';
|
|
|
|
export type DateRange = { since: string; until: string; label: string };
|
|
|
|
const QUICK: { id: RangePreset; label: string }[] = [
|
|
{ id: 'today', label: 'Hoy' },
|
|
{ id: 'yesterday', label: 'Ayer' },
|
|
{ id: 'last_7d', label: '7 días' },
|
|
{ id: 'last_14d', label: '14 días' },
|
|
{ id: 'last_28d', label: '28 días' },
|
|
{ id: 'last_30d', label: '30 días' },
|
|
{ id: 'this_week', label: 'Esta semana' },
|
|
{ id: 'last_week', label: 'Semana pasada' },
|
|
{ id: 'this_month', label: 'Este mes' },
|
|
{ id: 'last_month', label: 'Mes pasado' },
|
|
];
|
|
|
|
export { QUICK as AD_TRACKER_PRESETS };
|
|
|
|
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();
|
|
const diff = day === 0 ? -6 : 1 - day;
|
|
x.setUTCDate(x.getUTCDate() + diff);
|
|
return x;
|
|
}
|
|
|
|
const MONTHS_ES = [
|
|
'ene',
|
|
'feb',
|
|
'mar',
|
|
'abr',
|
|
'may',
|
|
'jun',
|
|
'jul',
|
|
'ago',
|
|
'sep',
|
|
'oct',
|
|
'nov',
|
|
'dic',
|
|
];
|
|
|
|
export function formatRangeLabel(since: string, until: string): string {
|
|
const a = since.split('-').map(Number);
|
|
const b = until.split('-').map(Number);
|
|
if (a.length !== 3 || b.length !== 3) return `${since} - ${until}`;
|
|
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 },
|
|
now = new Date()
|
|
): DateRange {
|
|
const today = utcToday(now);
|
|
const yesterday = utcYesterday(now);
|
|
/** Anchor end for non-today presets. */
|
|
const end = yesterday;
|
|
|
|
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(yesterday);
|
|
until = new Date(yesterday);
|
|
break;
|
|
case 'last_7d':
|
|
since = new Date(end);
|
|
since.setUTCDate(since.getUTCDate() - 6);
|
|
until = new Date(end);
|
|
break;
|
|
case 'last_14d':
|
|
since = new Date(end);
|
|
since.setUTCDate(since.getUTCDate() - 13);
|
|
until = new Date(end);
|
|
break;
|
|
case 'last_28d':
|
|
since = new Date(end);
|
|
since.setUTCDate(since.getUTCDate() - 27);
|
|
until = new Date(end);
|
|
break;
|
|
case 'last_30d':
|
|
since = new Date(end);
|
|
since.setUTCDate(since.getUTCDate() - 29);
|
|
until = new Date(end);
|
|
break;
|
|
case 'this_week':
|
|
since = startOfWeekMon(today);
|
|
until = new Date(end);
|
|
if (since.getTime() > until.getTime()) {
|
|
since = new Date(until);
|
|
}
|
|
break;
|
|
case 'last_week': {
|
|
const thisMon = startOfWeekMon(today);
|
|
until = new Date(thisMon);
|
|
until.setUTCDate(until.getUTCDate() - 1);
|
|
since = startOfWeekMon(until);
|
|
break;
|
|
}
|
|
case 'this_month':
|
|
since = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), 1));
|
|
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(
|
|
Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), 1)
|
|
);
|
|
until = new Date(firstThis);
|
|
until.setUTCDate(until.getUTCDate() - 1);
|
|
since = new Date(Date.UTC(until.getUTCFullYear(), until.getUTCMonth(), 1));
|
|
break;
|
|
}
|
|
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);
|
|
const u = iso(until);
|
|
return { since: s, until: u, label: formatRangeLabel(s, u) };
|
|
}
|
|
|
|
export function normalizePlatform(p?: string): 'meta' | 'google' | 'other' {
|
|
const x = (p || '').toLowerCase();
|
|
if (
|
|
x.includes('meta') ||
|
|
x.includes('facebook') ||
|
|
x.includes('instagram') ||
|
|
x === 'fb' ||
|
|
x === 'ig'
|
|
) {
|
|
return 'meta';
|
|
}
|
|
if (x.includes('google') || x.includes('adwords') || x === 'gads') {
|
|
return 'google';
|
|
}
|
|
return 'other';
|
|
}
|