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>
This commit is contained in:
116
src/app/api/calendar/linkedin-strategy/route.ts
Normal file
116
src/app/api/calendar/linkedin-strategy/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 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 || [],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user