feat: interactive seguimiento timeline and channel evolution
Sync Superhuman Docs seguimiento/KPI pages into the portal (never passwords) and show clients a dynamic timeline plus per-channel kpi_weekly charts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
107
src/app/api/seguimiento/route.ts
Normal file
107
src/app/api/seguimiento/route.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
|
||||
import { isAgencyRole, userRole } from '@/lib/agency';
|
||||
import {
|
||||
AdDailyRow,
|
||||
KpiWeeklyRow,
|
||||
buildChannelSeries,
|
||||
} from '@/lib/metrics';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export type SeguimientoEntry = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
entry_type: string;
|
||||
title: string;
|
||||
summary?: string | null;
|
||||
body_md?: string | null;
|
||||
browser_link?: string | null;
|
||||
metrics?: Record<string, unknown>;
|
||||
occurred_at?: string | null;
|
||||
synced_at?: string;
|
||||
};
|
||||
|
||||
/** GET seguimiento timeline + channel evolution for active tenant. */
|
||||
export async function GET() {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const ctx = await resolveActiveTenant(user);
|
||||
const agency = isAgencyRole(userRole(user));
|
||||
|
||||
if (agency && ctx.portfolioWide) {
|
||||
return NextResponse.json({
|
||||
label: ctx.label,
|
||||
portfolioWide: true,
|
||||
entries: [],
|
||||
channelSeries: { weeks: [], channels: [], points: [] },
|
||||
hint: 'Elige un cliente en el selector para ver su seguimiento y evolución de canales.',
|
||||
});
|
||||
}
|
||||
|
||||
const tenantId = ctx.tenantId;
|
||||
if (!tenantId) {
|
||||
return NextResponse.json({ error: 'Sin tenant' }, { status: 400 });
|
||||
}
|
||||
|
||||
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=80`
|
||||
),
|
||||
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.asc&limit=400`
|
||||
),
|
||||
]);
|
||||
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(80),
|
||||
supabase.from('kpi_weekly').select('*').order('week', { ascending: true }),
|
||||
supabase
|
||||
.from('ad_daily')
|
||||
.select('*')
|
||||
.order('date', { ascending: true })
|
||||
.limit(400),
|
||||
]);
|
||||
entries = (e as SeguimientoEntry[]) || [];
|
||||
weekly = (w as KpiWeeklyRow[]) || [];
|
||||
daily = (d as AdDailyRow[]) || [];
|
||||
}
|
||||
|
||||
const channelSeries = buildChannelSeries(weekly);
|
||||
|
||||
return NextResponse.json({
|
||||
label: ctx.label,
|
||||
portfolioWide: false,
|
||||
tenantId,
|
||||
entries,
|
||||
channelSeries,
|
||||
dailySummary: {
|
||||
days: daily.length,
|
||||
spend: daily.reduce((a, r) => a + Number(r.spend || 0), 0),
|
||||
leads: daily.reduce((a, r) => a + Number(r.platform_leads || 0), 0),
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user