feat: RRSS hub estilo Miia + Ad Tracker por cliente

Replica agua-miia/rrss (hub, archivo, octubre 2026 con 27 piezas) y añade Ad Tracker por tenant al estilo Brava (periodos, Meta+Google, CPL/clics, campañas).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-23 02:55:14 +02:00
parent a1a3c26c30
commit b4ee23874e
10 changed files with 2455 additions and 38 deletions

153
src/lib/ad-tracker-range.ts Normal file
View File

@@ -0,0 +1,153 @@
/** Date presets for Ad Tracker (Brava-style). No date-fns dependency. */
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);
}
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]}`;
}
export function resolveAdRange(
preset: RangePreset,
custom?: { since?: string; until?: string }
): DateRange {
const now = new Date();
const today = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
);
let since = today;
let until = today;
switch (preset) {
case 'today':
break;
case 'yesterday':
since = new Date(today);
since.setUTCDate(since.getUTCDate() - 1);
until = new Date(since);
break;
case 'last_7d':
since = new Date(today);
since.setUTCDate(since.getUTCDate() - 6);
break;
case 'last_14d':
since = new Date(today);
since.setUTCDate(since.getUTCDate() - 13);
break;
case 'last_28d':
since = new Date(today);
since.setUTCDate(since.getUTCDate() - 27);
break;
case 'last_30d':
since = new Date(today);
since.setUTCDate(since.getUTCDate() - 29);
break;
case 'this_week':
since = startOfWeekMon(today);
until = today;
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 = today;
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':
since = custom?.since ? new Date(custom.since + 'T00:00:00Z') : since;
until = custom?.until ? new Date(custom.until + 'T00:00:00Z') : until;
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';
}