feat: periodo global, métricas negocio, seguimiento notas, Drive/Social/Orgánico

Selector de fechas en header (con Personalizado) compartido en el dashboard; Métricas con Ventas/CAC/ROAS CRM, cruce canal y gráficas diarias; Seguimiento solo notas + sync Superhuman; Activos con navegación Drive; Social con métricas IG/FB; Orgánico con prioridades SEO/GEO expertas.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 15:45:30 +02:00
parent eb1622c88c
commit 3425364b07
15 changed files with 1067 additions and 1011 deletions

View File

@@ -7,14 +7,6 @@ import {
supabaseUrl,
userRole,
} from '@/lib/agency';
import {
AdDailyRow,
KpiWeeklyRow,
buildChannelSeries,
resolveWeeklyRows,
sumDaily,
sumWeekly,
} from '@/lib/metrics';
import { randomUUID } from 'crypto';
export const dynamic = 'force-dynamic';
@@ -33,7 +25,7 @@ export type SeguimientoEntry = {
source?: string;
};
/** GET seguimiento timeline + metrics series for active tenant. */
/** GET — notas + histórico Superhuman (sin series ads). */
export async function GET() {
const supabase = await createClient();
const {
@@ -51,10 +43,7 @@ export async function GET() {
label: ctx.label,
portfolioWide: true,
entries: [],
weekly: [],
daily: [],
channelSeries: { weeks: [], channels: [], points: [] },
hint: 'Elige un cliente en el selector para ver su seguimiento y evolución.',
hint: 'Elige un cliente en el selector para ver su seguimiento.',
});
}
@@ -64,78 +53,33 @@ export async function GET() {
}
let entries: SeguimientoEntry[] = [];
let weekly: KpiWeeklyRow[] = [];
let daily: AdDailyRow[] = [];
if (agency && ctx.viaView) {
const tid = encodeURIComponent(tenantId);
const [e, w, d] = await Promise.all([
restGet<SeguimientoEntry[]>(
`/rest/v1/seguimiento_entries?tenant_id=eq.${tid}&select=*&order=occurred_at.desc.nullslast&limit=120`
),
restGet<KpiWeeklyRow[]>(
`/rest/v1/kpi_weekly?tenant_id=eq.${tid}&select=*&order=week.asc`
),
restGet<AdDailyRow[]>(
`/rest/v1/ad_daily?tenant_id=eq.${tid}&select=*&order=date.desc&limit=5000`
),
]);
const e = await restGet<SeguimientoEntry[]>(
`/rest/v1/seguimiento_entries?tenant_id=eq.${tid}&select=*&order=occurred_at.desc.nullslast&limit=200`
);
entries = e.data || [];
weekly = w.data || [];
daily = d.data || [];
} else {
const [{ data: e }, { data: w }, { data: d }] = await Promise.all([
supabase
.from('seguimiento_entries')
.select('*')
.order('occurred_at', { ascending: false, nullsFirst: false })
.limit(120),
supabase.from('kpi_weekly').select('*').order('week', { ascending: true }),
supabase
.from('ad_daily')
.select('*')
.order('date', { ascending: false })
.limit(5000),
]);
const { data: e } = await supabase
.from('seguimiento_entries')
.select('*')
.order('occurred_at', { ascending: false, nullsFirst: false })
.limit(200);
entries = (e as SeguimientoEntry[]) || [];
weekly = (w as KpiWeeklyRow[]) || [];
daily = (d as AdDailyRow[]) || [];
}
weekly = resolveWeeklyRows(weekly, daily);
const channelSeries = buildChannelSeries(weekly);
const totalsWeekly = sumWeekly(weekly);
const totalsDaily = sumDaily(daily);
return NextResponse.json({
label: ctx.label,
portfolioWide: false,
tenantId,
entries,
weekly,
daily,
channelSeries,
totals: {
spend: totalsWeekly.spend || totalsDaily.spend,
leads: totalsWeekly.leads || totalsDaily.leads,
cpl: totalsWeekly.cpl || (totalsDaily.leads > 0 ? totalsDaily.spend / totalsDaily.leads : 0),
cac: totalsWeekly.cac,
revenue: totalsWeekly.revenue,
roas: totalsWeekly.roas,
impressions: totalsDaily.impressions,
clicks: totalsDaily.clicks,
},
dailySummary: {
days: daily.length,
spend: totalsDaily.spend,
leads: totalsDaily.leads,
},
});
}
/**
* POST — add a manual note / comentario to seguimiento.
* { title?, body, occurred_at? }
* { title?, body, occurredAt? | occurred_at? }
*/
export async function POST(req: NextRequest) {
const supabase = await createClient();
@@ -168,10 +112,10 @@ export async function POST(req: NextRequest) {
typeof body.title === 'string' && body.title.trim()
? body.title.trim().slice(0, 200)
: `Nota · ${new Date().toLocaleDateString('es-ES')}`;
const occurredAt =
typeof body.occurred_at === 'string' && body.occurred_at
? body.occurred_at
: new Date().toISOString();
const occurredRaw =
(typeof body.occurredAt === 'string' && body.occurredAt) ||
(typeof body.occurred_at === 'string' && body.occurred_at) ||
new Date().toISOString();
const row = {
tenant_id: ctx.tenantId,
@@ -181,7 +125,7 @@ export async function POST(req: NextRequest) {
title,
summary: text.slice(0, 280),
body_md: text,
occurred_at: occurredAt,
occurred_at: occurredRaw,
synced_at: new Date().toISOString(),
metrics: {},
};