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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user