Add in-app editorial calendar creator with month zoom and preview.

Agency can pick a month, generate a brand/seasonality preview, and add/edit/delete pieces with the same fields as the creative pipeline, optionally syncing drafts there.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-08 18:12:10 +02:00
parent 501f8203e8
commit f316497650
7 changed files with 1700 additions and 8 deletions

View File

@@ -0,0 +1,73 @@
/**
* POST { month: 'YYYY-MM' } → editorial preview for active tenant.
*/
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 { asPlanObject } from '@/lib/calendar-documents';
import { generateMonthPreview } from '@/lib/calendar-generate';
import type { RrssMonthPlan } from '@/components/RrssMonthView';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
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 });
}
const ctx = await resolveActiveTenant(user);
if (!ctx.tenantId || ctx.portfolioWide) {
return NextResponse.json(
{ error: 'Selecciona un cliente en Contexto.' },
{ status: 400 }
);
}
const body = (await req.json().catch(() => ({}))) as { month?: string };
const month = String(body.month || '').slice(0, 7);
if (!/^\d{4}-\d{2}$/.test(month)) {
return NextResponse.json({ error: 'month YYYY-MM requerido' }, { status: 400 });
}
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 docs = await restGet<{ content?: unknown }[]>(
`/rest/v1/documents?tenant_id=eq.${encodeURIComponent(
ctx.tenantId
)}&type=eq.calendar&select=content&order=created_at.desc&limit=24`
);
const history: RrssMonthPlan[] = [];
for (const d of docs.data || []) {
const plan = asPlanObject(d.content) as RrssMonthPlan | null;
if (plan?.month && plan.month !== month && Array.isArray(plan.pieces)) {
history.push(plan as unknown as RrssMonthPlan);
}
}
const result = await generateMonthPreview({
ym: month,
slug,
label: ctx.label,
history,
});
return NextResponse.json({
ok: true,
plan: result.plan,
rationale: result.rationale,
source: result.source,
brand: slug,
});
}