feat: metrics dashboard with WoW/MoM/YoY and creatives pipeline

Rebuild KPIs as a real dashboard (period deltas, tenant zoom), clarify
objetivos/calendario/activos sources, and turn creatives into a status
pipeline with delete.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-22 13:45:41 +02:00
parent 791976a3d8
commit 8182402d96
11 changed files with 1002 additions and 263 deletions

70
src/lib/tenant-context.ts Normal file
View File

@@ -0,0 +1,70 @@
import { cookies } from 'next/headers';
import {
VIEW_TENANT_COOKIE,
VIEW_TENANT_NAME_COOKIE,
homeTenantId,
isAgencyRole,
serviceHeaders,
supabaseUrl,
userRole,
} from '@/lib/agency';
export type ResolvedTenant = {
tenantId: string | null;
/** true = agency zoomed into one client */
viaView: boolean;
/** true = agency with no client selected → all tenants */
portfolioWide: boolean;
label: string;
};
export async function resolveActiveTenant(user: {
app_metadata?: Record<string, unknown>;
user_metadata?: Record<string, unknown>;
} | null): Promise<ResolvedTenant> {
const role = userRole(user);
const cookieStore = await cookies();
const viewId = cookieStore.get(VIEW_TENANT_COOKIE)?.value || null;
const viewName = cookieStore.get(VIEW_TENANT_NAME_COOKIE)?.value || null;
if (isAgencyRole(role) && viewId) {
return {
tenantId: viewId,
viaView: true,
portfolioWide: false,
label: viewName || 'Cliente',
};
}
if (isAgencyRole(role) && !viewId) {
return {
tenantId: null,
viaView: false,
portfolioWide: true,
label: 'Portafolio (todos los clientes)',
};
}
return {
tenantId: homeTenantId(user),
viaView: false,
portfolioWide: false,
label:
(user?.user_metadata?.company as string) ||
(user?.app_metadata?.company as string) ||
'Cliente',
};
}
export async function restGet<T = unknown>(
path: string
): Promise<{ ok: boolean; data: T | null; error?: string }> {
const headers = serviceHeaders();
if (!headers) return { ok: false, data: null, error: 'no service role' };
const r = await fetch(`${supabaseUrl()}${path}`, {
headers,
cache: 'no-store',
});
if (!r.ok) {
return { ok: false, data: null, error: await r.text() };
}
return { ok: true, data: (await r.json()) as T };
}