feat: Zona de marca tipo Estilo/Brief + brandbooks desde ads/RRSS
UI con nav lateral numerada y panel editable; parse de brandbook JSON string; enrich desde Meta/Google/IG/FB/web/CSS para que app.4ventures.es muestre el kit. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
107
src/app/api/brandbook/enrich/route.ts
Normal file
107
src/app/api/brandbook/enrich/route.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* POST — auto-fill Zona de marca for one or all tenants from available sources.
|
||||
* Body: { tenant_id?, slug?, all?: true, force_seed?: true, dry_run?: true }
|
||||
*/
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { isAgencyRole, userRole } from '@/lib/agency';
|
||||
import { resolveActiveTenant } from '@/lib/tenant-context';
|
||||
import {
|
||||
enrichAllTenantBrandbooks,
|
||||
enrichTenantBrandbook,
|
||||
} from '@/lib/brandbook-enrich';
|
||||
import { ensureCreativesDriveFolders } from '@/lib/creatives-drive';
|
||||
import { serviceHeaders, supabaseUrl } from '@/lib/agency';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 120;
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
if (!isAgencyRole(userRole(user))) {
|
||||
return NextResponse.json({ error: 'Solo agencia' }, { status: 403 });
|
||||
}
|
||||
|
||||
let body: {
|
||||
tenant_id?: string;
|
||||
slug?: string;
|
||||
all?: boolean;
|
||||
force_seed?: boolean;
|
||||
dry_run?: boolean;
|
||||
ensure_drive?: boolean;
|
||||
} = {};
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
body = {};
|
||||
}
|
||||
|
||||
const forceSeed = Boolean(body.force_seed);
|
||||
const dryRun = Boolean(body.dry_run);
|
||||
const ensureDrive = body.ensure_drive !== false;
|
||||
|
||||
if (body.all) {
|
||||
const results = await enrichAllTenantBrandbooks({
|
||||
forceSeed,
|
||||
dryRun,
|
||||
slugFilter: body.slug,
|
||||
});
|
||||
if (ensureDrive && !dryRun) {
|
||||
for (const r of results) {
|
||||
if (!r.ok) continue;
|
||||
await ensureCreativesDriveFolders(r.tenantId, r.slug);
|
||||
}
|
||||
}
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
count: results.length,
|
||||
results,
|
||||
});
|
||||
}
|
||||
|
||||
let tenantId = body.tenant_id?.trim() || '';
|
||||
if (!tenantId && body.slug) {
|
||||
const headers = serviceHeaders();
|
||||
const base = supabaseUrl();
|
||||
if (headers && base) {
|
||||
const r = await fetch(
|
||||
`${base}/rest/v1/tenants?slug=eq.${encodeURIComponent(
|
||||
body.slug
|
||||
)}&select=id&limit=1`,
|
||||
{ headers, cache: 'no-store' }
|
||||
);
|
||||
const rows = (await r.json()) as { id: string }[];
|
||||
tenantId = rows[0]?.id || '';
|
||||
}
|
||||
}
|
||||
if (!tenantId) {
|
||||
const ctx = await resolveActiveTenant(user);
|
||||
tenantId = ctx.tenantId || '';
|
||||
}
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Indica tenant_id, slug, all:true o selecciona cliente' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await enrichTenantBrandbook({
|
||||
tenantId,
|
||||
forceSeed,
|
||||
dryRun,
|
||||
});
|
||||
|
||||
let drive: { ok: boolean; ads?: string; rrss?: string; warning?: string } | null =
|
||||
null;
|
||||
if (ensureDrive && !dryRun && result.ok) {
|
||||
drive = await ensureCreativesDriveFolders(result.tenantId, result.slug);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: result.ok, result, drive });
|
||||
}
|
||||
Reference in New Issue
Block a user