import { NextRequest, NextResponse } from 'next/server'; import { cookies } from 'next/headers'; import { createClient } from '@/lib/supabase/server'; import { VIEW_TENANT_COOKIE, homeTenantId, isAgencyRole, serviceHeaders, supabaseUrl, userRole, } from '@/lib/agency'; const SELECT = 'id,title,status,channel,placement,format,headline,primary_text,description,cta,media_url,brand_name,landing_url,storage_path,mime_type,bytes,created_at,updated_at'; const CHANNELS = new Set([ 'meta_ads', 'google_ads', 'instagram', 'facebook', 'linkedin', 'tiktok', 'stories', 'other', ]); const FORMATS = new Set([ 'image', 'video', 'carousel', 'reel', 'story', 'rsa', 'pmax', 'pmax_landscape', 'pmax_square', 'pmax_portrait', 'pmax_vertical', 'pmax_logo', 'youtube', 'other', ]); const ALLOWED_STATUS = new Set(['draft', 'in_review', 'approved', 'rejected']); async function resolveTenantId(user: { app_metadata?: Record; user_metadata?: Record; }): Promise<{ tenantId: string | null; viaView: boolean }> { const role = userRole(user); const cookieStore = await cookies(); const viewId = cookieStore.get(VIEW_TENANT_COOKIE)?.value || null; if (isAgencyRole(role) && viewId) { return { tenantId: viewId, viaView: true }; } return { tenantId: homeTenantId(user), viaView: false }; } function str(v: unknown, max = 2000): string | null { if (typeof v !== 'string') return null; const t = v.trim(); if (!t) return null; return t.slice(0, max); } function previewFields(body: Record) { const channel = str(body.channel, 40); const format = str(body.format, 40); return { channel: channel && CHANNELS.has(channel) ? channel : null, placement: str(body.placement, 40), format: format && FORMATS.has(format) ? format : null, headline: str(body.headline, 500), primary_text: str(body.primary_text, 8000), description: str(body.description, 2000), cta: str(body.cta, 120), media_url: str(body.media_url, 2000), brand_name: str(body.brand_name, 200), landing_url: str(body.landing_url, 2000), }; } export async function GET() { const supabase = await createClient(); const { data: { user }, } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { tenantId, viaView } = await resolveTenantId(user); if (viaView && tenantId) { const headers = serviceHeaders(); if (!headers) { return NextResponse.json( { creatives: [], warning: 'Service role required for superadmin view' }, { status: 200 } ); } const r = await fetch( `${supabaseUrl()}/rest/v1/creatives?tenant_id=eq.${encodeURIComponent(tenantId)}&select=${SELECT}&order=updated_at.desc`, { headers, cache: 'no-store' } ); if (!r.ok) { // Fallback if preview columns not migrated yet const fallback = await fetch( `${supabaseUrl()}/rest/v1/creatives?tenant_id=eq.${encodeURIComponent(tenantId)}&select=id,title,status,mime_type,bytes,created_at,updated_at&order=updated_at.desc`, { headers, cache: 'no-store' } ); if (fallback.ok) { return NextResponse.json({ creatives: await fallback.json(), viewTenantId: tenantId, warning: 'Aplica sql/creatives_preview.sql para canales, copy y media preview', }); } return NextResponse.json({ creatives: [], warning: await r.text(), viewTenantId: tenantId, }); } return NextResponse.json({ creatives: await r.json(), viewTenantId: tenantId, }); } const { data, error } = await supabase .from('creatives') .select(SELECT) .order('updated_at', { ascending: false }); if (error) { const { data: basic, error: e2 } = await supabase .from('creatives') .select('id,title,status,mime_type,bytes,created_at,updated_at') .order('updated_at', { ascending: false }); if (e2) { return NextResponse.json( { creatives: [], warning: error.message }, { status: 200 } ); } return NextResponse.json({ creatives: basic ?? [], warning: 'Aplica sql/creatives_preview.sql para canales, copy y media preview', }); } return NextResponse.json({ creatives: data ?? [] }); } 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 }); } const { tenantId, viaView } = await resolveTenantId(user); if (!tenantId) { return NextResponse.json({ error: 'Usuario sin tenant_id' }, { status: 403 }); } const body = (await req.json().catch(() => ({}))) as Record; const title = str(body.title, 300); if (!title) { return NextResponse.json({ error: 'title requerido' }, { status: 400 }); } const row = { tenant_id: tenantId, title, status: 'draft', created_by: user.id, ...previewFields(body), }; if (viaView) { const headers = serviceHeaders(); if (!headers) { return NextResponse.json( { error: 'Service role required' }, { status: 503 } ); } const r = await fetch(`${supabaseUrl()}/rest/v1/creatives`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json', Prefer: 'return=representation', }, body: JSON.stringify(row), }); if (!r.ok) { return NextResponse.json({ error: await r.text() }, { status: 500 }); } const data = await r.json(); return NextResponse.json( { creative: Array.isArray(data) ? data[0] : data }, { status: 201 } ); } const { data, error } = await supabase .from('creatives') .insert(row) .select(SELECT) .single(); if (error) { // Retry without preview columns if migration pending const { data: basic, error: e2 } = await supabase .from('creatives') .insert({ tenant_id: tenantId, title, status: 'draft', created_by: user.id, }) .select('id,title,status,created_at') .single(); if (e2) { return NextResponse.json({ error: error.message }, { status: 500 }); } return NextResponse.json( { creative: basic, warning: 'Preview columns missing — apply sql/creatives_preview.sql', }, { status: 201 } ); } return NextResponse.json({ creative: data }, { status: 201 }); } 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 body = (await req.json().catch(() => ({}))) as Record; const id = str(body.id, 80); if (!id) { return NextResponse.json({ error: 'id requerido' }, { status: 400 }); } const clean: Record = { updated_at: new Date().toISOString(), }; if (typeof body.status === 'string' && ALLOWED_STATUS.has(body.status)) { clean.status = body.status; } const title = str(body.title, 300); if (title) clean.title = title; for (const [k, v] of Object.entries(previewFields(body))) { if (k in body) clean[k] = v; } const agency = isAgencyRole(userRole(user)); const headers = serviceHeaders(); if (agency && headers) { const r = await fetch( `${supabaseUrl()}/rest/v1/creatives?id=eq.${encodeURIComponent(id)}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json', Prefer: 'return=representation', }, body: JSON.stringify(clean), } ); if (!r.ok) { return NextResponse.json({ error: await r.text() }, { status: 500 }); } const data = await r.json(); return NextResponse.json({ creative: data[0] || data }); } const { data, error } = await supabase .from('creatives') .update(clean) .eq('id', id) .select(SELECT) .single(); if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } return NextResponse.json({ creative: data }); } export async function DELETE(req: NextRequest) { const supabase = await createClient(); const { data: { user }, } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const id = req.nextUrl.searchParams.get('id')?.trim() || ''; if (!id) { return NextResponse.json({ error: 'id requerido' }, { status: 400 }); } const agency = isAgencyRole(userRole(user)); const headers = serviceHeaders(); if (agency && headers) { const r = await fetch( `${supabaseUrl()}/rest/v1/creatives?id=eq.${encodeURIComponent(id)}`, { method: 'DELETE', headers } ); if (!r.ok) { return NextResponse.json({ error: await r.text() }, { status: 500 }); } return NextResponse.json({ ok: true }); } const { error } = await supabase.from('creatives').delete().eq('id', id); if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } return NextResponse.json({ ok: true }); }