feat: KPIs con totales absolutos, backfill ads y ventas en cruce canal

Comparativas WoW/MoM/YoY muestran prev/Δ además del %; sync ads admite
histórico desde 2025; tabla ads↔CRM añade cierres, ventas y ROAS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-28 10:23:36 +02:00
parent dd50251de2
commit cf27eba74d
30 changed files with 2367 additions and 514 deletions

View File

@@ -20,7 +20,7 @@ export async function POST(req: NextRequest) {
const tenantId =
typeof body.tenantId === 'string' ? body.tenantId : undefined;
const days =
typeof body.days === 'number' && body.days > 0 && body.days <= 90
typeof body.days === 'number' && body.days > 0 && body.days <= 1000
? body.days
: 30;

View File

@@ -7,14 +7,8 @@ import {
userRole,
} from '@/lib/agency';
import {
SUPERHUMAN_PROVIDER,
bestDocForTenant,
buildSeguimientoRowsForDoc,
listDocs,
matchDocsToTenants,
patchSuperhumanIntegration,
syncAllSuperhumanTenants,
superhumanConfigured,
upsertSeguimientoRows,
} from '@/lib/superhuman-docs';
export const dynamic = 'force-dynamic';
@@ -51,6 +45,9 @@ export async function GET() {
if ('error' in gate && gate.error) return gate.error;
try {
const { listDocs, matchDocsToTenants, SUPERHUMAN_PROVIDER } = await import(
'@/lib/superhuman-docs'
);
const headers = (gate as { headers: { apikey: string; Authorization: string } })
.headers;
const [docs, tenantsRes] = await Promise.all([
@@ -91,8 +88,6 @@ export async function GET() {
export async function POST(req: NextRequest) {
const gate = await requireAgency();
if ('error' in gate && gate.error) return gate.error;
const headers = (gate as { headers: { apikey: string; Authorization: string } })
.headers;
const body = await req.json().catch(() => ({}));
const onlyTenant: string | undefined = body.tenantId
@@ -102,128 +97,14 @@ export async function POST(req: NextRequest) {
const maxEntries = Math.min(Number(body.maxEntries) || 80, 150);
try {
const docs = await listDocs();
const tRes = await fetch(
`${supabaseUrl()}/rest/v1/tenants?select=id,name,slug&order=name.asc`,
{ headers, cache: 'no-store' }
);
if (!tRes.ok) {
return NextResponse.json({ error: await tRes.text() }, { status: 500 });
}
let tenants = (await tRes.json()) as {
id: string;
name: string;
slug: string;
}[];
if (onlyTenant) {
tenants = tenants.filter((t) => t.id === onlyTenant);
}
const results: {
tenantId: string;
tenantName: string;
docId: string | null;
docName: string | null;
upserted: number;
status: string;
error?: string;
}[] = [];
for (const tenant of tenants) {
let docId: string | null = null;
let docName: string | null = null;
// Prefer already-linked integration
const integUrl = `${supabaseUrl()}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tenant.id)}&provider=eq.${SUPERHUMAN_PROVIDER}&select=external_account_id,meta,label`;
const integR = await fetch(integUrl, { headers, cache: 'no-store' });
const integRows = integR.ok
? ((await integR.json()) as {
external_account_id?: string | null;
meta?: { doc_name?: string };
}[])
: [];
if (integRows[0]?.external_account_id) {
docId = integRows[0].external_account_id;
docName =
integRows[0].meta?.doc_name ||
docs.find((d) => d.id === docId)?.name ||
null;
} else {
const hit = bestDocForTenant(tenant, docs);
if (hit) {
docId = hit.doc.id;
docName = hit.doc.name;
}
}
if (!docId) {
await patchSuperhumanIntegration(tenant.id, {
status: 'disconnected',
error_message: 'Sin doc Superhuman matched por nombre',
});
results.push({
tenantId: tenant.id,
tenantName: tenant.name,
docId: null,
docName: null,
upserted: 0,
status: 'unmatched',
});
continue;
}
try {
const rows = await buildSeguimientoRowsForDoc({
tenantId: tenant.id,
tenantName: tenant.name,
docId,
maxEntries,
exportBodies,
});
const upserted = await upsertSeguimientoRows(rows);
await patchSuperhumanIntegration(tenant.id, {
status: 'connected',
external_account_id: docId,
label: docName || 'Superhuman Docs',
last_sync_at: new Date().toISOString(),
error_message: null,
meta: {
doc_name: docName,
entries: upserted,
synced_at: new Date().toISOString(),
},
});
results.push({
tenantId: tenant.id,
tenantName: tenant.name,
docId,
docName,
upserted,
status: 'synced',
});
} catch (e) {
const msg = e instanceof Error ? e.message : 'sync failed';
await patchSuperhumanIntegration(tenant.id, {
status: 'error',
external_account_id: docId,
error_message: msg.slice(0, 400),
});
results.push({
tenantId: tenant.id,
tenantName: tenant.name,
docId,
docName,
upserted: 0,
status: 'error',
error: msg,
});
}
}
const result = await syncAllSuperhumanTenants({
tenantId: onlyTenant || null,
maxEntries,
exportBodies,
});
return NextResponse.json({
ok: true,
results,
synced: results.filter((r) => r.status === 'synced').length,
...result,
});
} catch (e) {
return NextResponse.json(