Files
4ventures-client-portal/src/app/api/calendar/linkedin-strategy/route.ts
4Ventures 4a1abd3318 feat: LinkedIn editorial, accesos agencia estables y deshacer en historial
Integra estrategias LinkedIn en calendarios (backlog publicado vs pendiente), unifica roles/cookies de vista agencia y permite deshacer cambios de brandbook dejando constancia en el historial.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 04:15:51 +02:00

117 lines
3.4 KiB
TypeScript

/**
* GET — resolve LinkedIn strategy URL + summary for active tenant
* (brandbook URL, known slug map, optional ?url= override).
* Used by CalendarCreator / zoom to show associated strategy content.
*/
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
import { isAgencyRole, userRole } from '@/lib/agency';
import {
BRANDBOOK_DOC_TYPE,
parseBrandbook,
} from '@/lib/brandbook';
import {
loadLinkedInStrategyContext,
resolveLinkedInStrategyUrl,
} from '@/lib/linkedin-strategy';
export const dynamic = 'force-dynamic';
export const maxDuration = 45;
export async function GET(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 });
}
const ctx = await resolveActiveTenant(user);
if (!ctx.tenantId || ctx.portfolioWide) {
return NextResponse.json(
{ error: 'Selecciona un cliente en Contexto.' },
{ status: 400 }
);
}
const override = req.nextUrl.searchParams.get('url') || '';
const postId = req.nextUrl.searchParams.get('post_id') || '';
let slug = 'cliente';
const t = await restGet<{ slug?: string }[]>(
`/rest/v1/tenants?id=eq.${encodeURIComponent(ctx.tenantId)}&select=slug`
);
slug = t.data?.[0]?.slug || ctx.label.toLowerCase().replace(/\s+/g, '-');
const bb = await restGet<{ content?: unknown }[]>(
`/rest/v1/documents?tenant_id=eq.${encodeURIComponent(
ctx.tenantId
)}&type=eq.${BRANDBOOK_DOC_TYPE}&select=content&order=updated_at.desc&limit=1`
);
const brandbook = parseBrandbook(bb.data?.[0]?.content);
const url = resolveLinkedInStrategyUrl({
brandbook,
slug,
overrideUrl: override,
});
if (!url) {
return NextResponse.json({
ok: true,
url: null,
brandbook_url: brandbook?.linkedin_strategy_url || null,
posts: [],
themes: brandbook?.linkedin_themes || [],
formats: brandbook?.linkedin_formats || [],
ctas: brandbook?.linkedin_cta || [],
message: 'Sin URL de estrategia LinkedIn en brandbook ni mapa conocido.',
});
}
const ctxLi = await loadLinkedInStrategyContext({
url,
brandbook,
mode: 'from_strategy',
});
const posts = (ctxLi?.posts || []).map((p) => ({
id: p.id,
n: p.n,
title: p.title,
meta: p.meta,
angle: p.angle,
body_preview: (p.body || '').slice(0, 500),
body: postId && p.id === postId ? p.body : undefined,
}));
let selected = postId
? ctxLi?.posts.find((p) => p.id === postId) || null
: null;
return NextResponse.json({
ok: true,
url,
brandbook_url: brandbook?.linkedin_strategy_url || null,
posts_count: ctxLi?.posts.length || 0,
posts: posts.slice(0, 80),
selected: selected
? {
id: selected.id,
title: selected.title,
meta: selected.meta,
angle: selected.angle,
body: selected.body,
}
: null,
themes: brandbook?.linkedin_themes || [],
formats: brandbook?.linkedin_formats || [],
ctas: brandbook?.linkedin_cta || [],
thought_leadership: brandbook?.thought_leadership || [],
});
}