import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { resolveActiveTenant } from '@/lib/tenant-context'; import { isAgencyRole, serviceHeaders, supabaseUrl, userRole, } from '@/lib/agency'; import type { RrssPiece } from '@/components/RrssMonthView'; import { emptyMonthPlan } from '@/lib/calendar-documents'; export const dynamic = 'force-dynamic'; export const runtime = 'nodejs'; type PlanDoc = { id: string; title: string; content: unknown; }; function asObject(content: unknown): Record | null { let c = content; if (typeof c === 'string') { try { c = JSON.parse(c); } catch { return null; } } if (!c || typeof c !== 'object') return null; return c as Record; } function piecesOf(content: Record): RrssPiece[] { return Array.isArray(content.pieces) ? (content.pieces as RrssPiece[]) : []; } function monthTitle(ym: string): string { const labels = [ 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre', ]; const m = Number(ym.slice(5, 7)) - 1; return `RRSS ${labels[m] || ym} ${ym.slice(0, 4)}`; } async function patchDoc( base: string, headers: Record, id: string, plan: Record ) { return fetch(`${base}/rest/v1/documents?id=eq.${encodeURIComponent(id)}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json', Prefer: 'return=minimal', }, body: JSON.stringify({ content: plan, updated_at: new Date().toISOString(), }), }); } /** Keep creative_pipeline.scheduled_at in sync so overlays don't snap back. */ async function syncPipelineSchedule( base: string, headers: Record, tenantId: string, pieceId: string, date: string, time?: string ) { const hhmm = (time || '10:00').slice(0, 5); const scheduledAt = new Date(`${date}T${hhmm}:00`).toISOString(); const pipeId = pieceId.startsWith('pipe-') ? pieceId.slice(5) : null; const filters = pipeId ? `id=eq.${encodeURIComponent(pipeId)}` : `calendar_piece_id=eq.${encodeURIComponent(pieceId)}`; await fetch( `${base}/rest/v1/creative_pipeline?tenant_id=eq.${encodeURIComponent( tenantId )}&${filters}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json', Prefer: 'return=minimal', }, body: JSON.stringify({ scheduled_at: scheduledAt, // Mover en calendario = reprogramar: el cron debe poder publicarla status: 'aprobada', publish_status: 'queued', publish_error: null, updated_at: new Date().toISOString(), }), } ); } export async function PATCH(req: NextRequest) { const supabase = await createClient(); const { data: { user }, } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const agency = isAgencyRole(userRole(user)); if (!agency) { return NextResponse.json( { error: 'Solo la agencia puede mover piezas' }, { 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(() => null)) as { pieceId?: string; date?: string; snapshot?: RrssPiece; } | null; const pieceId = String(body?.pieceId || '').trim(); const date = String(body?.date || '').slice(0, 10); if (!pieceId || !/^\d{4}-\d{2}-\d{2}$/.test(date)) { return NextResponse.json( { error: 'Falta pieceId o una fecha vĂ¡lida.' }, { status: 400 } ); } const headers = serviceHeaders(); const base = supabaseUrl(); if (!headers || !base) { return NextResponse.json({ error: 'Service role required' }, { status: 503 }); } const list = await fetch( `${base}/rest/v1/documents?tenant_id=eq.${encodeURIComponent( ctx.tenantId )}&type=eq.calendar&select=id,title,content`, { headers, cache: 'no-store' } ); const docs = (list.ok ? await list.json() : []) as PlanDoc[]; let source: { doc: PlanDoc; plan: Record; index: number } | null = null; for (const doc of docs) { const plan = asObject(doc.content); if (!plan) continue; const pieces = piecesOf(plan); const index = pieces.findIndex((p) => p.id === pieceId); if (index >= 0) { source = { doc, plan, index }; break; } } const ym = date.slice(0, 7); let targetDoc: PlanDoc | null = source?.doc || null; let targetPlan: Record | null = source?.plan || null; const sourceMonth = source ? String(source.plan.month || '') : ''; // Same-document move when the piece already lives in the target month doc // (also covers old imports where plan.month was missing). if ( source && (sourceMonth === ym || (!sourceMonth && piecesOf(source.plan).some((p) => String(p.date || '').startsWith(ym) ))) ) { targetDoc = source.doc; targetPlan = source.plan; if (!targetPlan.month) targetPlan.month = ym; } else if (!targetDoc || sourceMonth !== ym) { targetDoc = null; targetPlan = null; for (const doc of docs) { const plan = asObject(doc.content); if (!plan) continue; if (String(plan.month || '') === ym) { targetDoc = doc; targetPlan = plan; break; } } } // Auto-create month doc when dragging into a month that has no calendar yet. if (!targetDoc || !targetPlan) { const plan = emptyMonthPlan(ym, ctx.label || 'Cliente'); const create = await fetch(`${base}/rest/v1/documents`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json', Prefer: 'return=representation', }, body: JSON.stringify({ tenant_id: ctx.tenantId, type: 'calendar', title: monthTitle(ym), content: plan, }), }); if (!create.ok) { return NextResponse.json( { error: 'No hay calendario para ese mes y no se pudo crear uno nuevo.', }, { status: 404 } ); } const created = (await create.json()) as PlanDoc[]; targetDoc = Array.isArray(created) ? created[0] : created; targetPlan = plan as unknown as Record; if (!targetDoc?.id) { return NextResponse.json( { error: 'No hay un calendario guardado para ese mes.' }, { status: 404 } ); } } const targetPieces = piecesOf(targetPlan); const movedTime = source?.plan ? piecesOf(source.plan)[source.index]?.time : body?.snapshot?.time; if (source && source.doc.id === targetDoc.id) { targetPieces[source.index] = { ...targetPieces[source.index], date, platform: String(targetPieces[source.index].platform || 'instagram') .trim() .toLowerCase(), }; } else if (source) { const moved = { ...piecesOf(source.plan)[source.index], date, platform: String( piecesOf(source.plan)[source.index]?.platform || 'instagram' ) .trim() .toLowerCase(), }; piecesOf(source.plan).splice(source.index, 1); source.plan.pieces = piecesOf(source.plan); if (!source.plan.month && sourceMonth) source.plan.month = sourceMonth; const srcPatch = await patchDoc(base, headers, source.doc.id, source.plan); if (!srcPatch.ok) { return NextResponse.json( { error: 'No se pudo actualizar el mes de origen.' }, { status: 500 } ); } targetPieces.push(moved); } else if (body?.snapshot) { targetPieces.push({ ...body.snapshot, date, id: pieceId, platform: String(body.snapshot.platform || 'instagram') .trim() .toLowerCase(), }); } else { return NextResponse.json( { error: 'No se encontrĂ³ esa pieza en el calendario.' }, { status: 404 } ); } targetPlan.month = ym; targetPlan.pieces = targetPieces.sort((a, b) => String(a.date).localeCompare(String(b.date)) ); const patch = await patchDoc(base, headers, targetDoc.id, targetPlan); if (!patch.ok) { return NextResponse.json( { error: (await patch.text()).slice(0, 240) || 'No se pudo guardar.' }, { status: 500 } ); } await syncPipelineSchedule( base, headers, ctx.tenantId, pieceId, date, movedTime ); return NextResponse.json({ ok: true, pieceId, date }); }