import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { createClient as createAdminClient } from '@supabase/supabase-js'; import { isAgencyRole, supabaseUrl, userRole } from '@/lib/agency'; import { resolveActiveTenant } from '@/lib/tenant-context'; import { randomUUID } from 'crypto'; export const dynamic = 'force-dynamic'; export type ContactMethod = 'Email' | 'Grupo WhatsApp' | 'Teams'; export interface ClientContact { id: string; name: string; role: string; email: string; phone?: string | null; contact_methods: ContactMethod[]; notes?: string | null; created_at: string; updated_at?: string; created_by?: string; } const DOC_TYPE = 'client_contacts'; function getAdmin() { const url = supabaseUrl(); const key = process.env.SUPABASE_SERVICE_ROLE_KEY!; return createAdminClient(url, key); } async function gate(req: NextRequest) { const supabase = await createClient(); const { data: { user }, } = await supabase.auth.getUser(); const apiKey = req.headers.get('x-api-key'); const validApiKey = process.env.CRON_SECRET || '4v-cron-sec-2026-mkt'; const isApiAuth = Boolean(apiKey && apiKey === validApiKey); if (!user && !isApiAuth) { return { error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) }; } const queryTenant = req.nextUrl.searchParams.get('tenant_id') || req.nextUrl.searchParams.get('tenantId'); const role = user ? userRole(user) : 'admin'; const isAgency = user ? isAgencyRole(role) : true; let tenantId: string | null = null; let label = 'Cliente'; if ((isAgency || isApiAuth) && queryTenant) { tenantId = queryTenant; const admin = getAdmin(); const { data: t } = await admin .from('tenants') .select('name') .eq('id', queryTenant) .maybeSingle(); if (t?.name) label = t.name; } else if (user) { const ctx = await resolveActiveTenant(user); if (ctx.portfolioWide || !ctx.tenantId) { return { error: NextResponse.json( { error: 'Selecciona un cliente en Contexto antes de gestionar el directorio.' }, { status: 400 } ), }; } tenantId = ctx.tenantId; label = ctx.label; } if (!tenantId) { return { error: NextResponse.json( { error: 'No se pudo identificar el tenant objetivo.' }, { status: 400 } ), }; } return { user, tenantId, label, isAgency }; } /** * GET /api/assets/contacts * Lista las personas del directorio del cliente activo. */ export async function GET(req: NextRequest) { const g = await gate(req); if ('error' in g) return g.error; const admin = getAdmin(); const { data, error } = await admin .from('documents') .select('id, content, updated_at') .eq('tenant_id', g.tenantId) .eq('type', DOC_TYPE) .order('updated_at', { ascending: false }) .limit(1) .maybeSingle(); if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } let contacts: ClientContact[] = []; if (data?.content) { try { contacts = JSON.parse(data.content); } catch { contacts = []; } } return NextResponse.json({ contacts, label: g.label }); } /** * POST /api/assets/contacts * Añade una nueva persona al directorio del cliente. */ export async function POST(req: NextRequest) { const g = await gate(req); if ('error' in g) return g.error; const body = await req.json().catch(() => null); if (!body) { return NextResponse.json({ error: 'Cuerpo de petición inválido' }, { status: 400 }); } const { name, role, email, phone, contact_methods, notes } = body; if (!name || typeof name !== 'string' || !name.trim()) { return NextResponse.json({ error: 'El Nombre y Apellido es obligatorio.' }, { status: 400 }); } if (!role || typeof role !== 'string' || !role.trim()) { return NextResponse.json({ error: 'El Cargo es obligatorio.' }, { status: 400 }); } if (!email || typeof email !== 'string' || !email.trim()) { return NextResponse.json({ error: 'El Email es obligatorio.' }, { status: 400 }); } const validMethods: ContactMethod[] = ['Email', 'Grupo WhatsApp', 'Teams']; const methods: ContactMethod[] = Array.isArray(contact_methods) ? contact_methods.filter((m: any): m is ContactMethod => validMethods.includes(m)) : []; const admin = getAdmin(); // Obtener registro existente const { data: existingDoc } = await admin .from('documents') .select('id, content') .eq('tenant_id', g.tenantId) .eq('type', DOC_TYPE) .order('updated_at', { ascending: false }) .limit(1) .maybeSingle(); let contacts: ClientContact[] = []; if (existingDoc?.content) { try { contacts = JSON.parse(existingDoc.content); } catch { contacts = []; } } const now = new Date().toISOString(); const newContact: ClientContact = { id: randomUUID(), name: name.trim(), role: role.trim(), email: email.trim().toLowerCase(), phone: phone && typeof phone === 'string' && phone.trim() ? phone.trim() : null, contact_methods: methods.length > 0 ? methods : ['Email'], notes: notes && typeof notes === 'string' && notes.trim() ? notes.trim() : null, created_at: now, updated_at: now, created_by: g.user?.email || (g.isAgency ? 'Agencia' : 'Cliente'), }; contacts.unshift(newContact); if (existingDoc) { const { error: updateErr } = await admin .from('documents') .update({ content: JSON.stringify(contacts), title: 'Directorio de Personas del Cliente', updated_at: now, }) .eq('id', existingDoc.id); if (updateErr) { return NextResponse.json({ error: updateErr.message }, { status: 500 }); } } else { const { error: insertErr } = await admin .from('documents') .insert({ tenant_id: g.tenantId, type: DOC_TYPE, title: 'Directorio de Personas del Cliente', content: JSON.stringify(contacts), created_at: now, updated_at: now, }); if (insertErr) { return NextResponse.json({ error: insertErr.message }, { status: 500 }); } } return NextResponse.json({ ok: true, contact: newContact, contacts }); } /** * PUT /api/assets/contacts * Actualiza una persona existente. */ export async function PUT(req: NextRequest) { const g = await gate(req); if ('error' in g) return g.error; const body = await req.json().catch(() => null); if (!body || !body.id) { return NextResponse.json({ error: 'ID de persona obligatorio.' }, { status: 400 }); } const { id, name, role, email, phone, contact_methods, notes } = body; if (!name || typeof name !== 'string' || !name.trim()) { return NextResponse.json({ error: 'El Nombre y Apellido es obligatorio.' }, { status: 400 }); } if (!role || typeof role !== 'string' || !role.trim()) { return NextResponse.json({ error: 'El Cargo es obligatorio.' }, { status: 400 }); } if (!email || typeof email !== 'string' || !email.trim()) { return NextResponse.json({ error: 'El Email es obligatorio.' }, { status: 400 }); } const validMethods: ContactMethod[] = ['Email', 'Grupo WhatsApp', 'Teams']; const methods: ContactMethod[] = Array.isArray(contact_methods) ? contact_methods.filter((m: any): m is ContactMethod => validMethods.includes(m)) : []; const admin = getAdmin(); const { data: existingDoc } = await admin .from('documents') .select('id, content') .eq('tenant_id', g.tenantId) .eq('type', DOC_TYPE) .order('updated_at', { ascending: false }) .limit(1) .maybeSingle(); if (!existingDoc?.content) { return NextResponse.json({ error: 'No se encontró el directorio de contactos' }, { status: 404 }); } let contacts: ClientContact[] = []; try { contacts = JSON.parse(existingDoc.content); } catch { contacts = []; } const idx = contacts.findIndex((c) => c.id === id); if (idx === -1) { return NextResponse.json({ error: 'Contacto no encontrado' }, { status: 404 }); } const now = new Date().toISOString(); contacts[idx] = { ...contacts[idx], name: name.trim(), role: role.trim(), email: email.trim().toLowerCase(), phone: phone && typeof phone === 'string' && phone.trim() ? phone.trim() : null, contact_methods: methods.length > 0 ? methods : ['Email'], notes: notes && typeof notes === 'string' && notes.trim() ? notes.trim() : null, updated_at: now, }; const { error: updateErr } = await admin .from('documents') .update({ content: JSON.stringify(contacts), updated_at: now, }) .eq('id', existingDoc.id); if (updateErr) { return NextResponse.json({ error: updateErr.message }, { status: 500 }); } return NextResponse.json({ ok: true, contact: contacts[idx], contacts }); } /** * DELETE /api/assets/contacts?id=... * Elimina una persona del directorio. */ export async function DELETE(req: NextRequest) { const g = await gate(req); if ('error' in g) return g.error; const url = new URL(req.url); const id = url.searchParams.get('id'); if (!id) { return NextResponse.json({ error: 'ID de persona obligatorio.' }, { status: 400 }); } const admin = getAdmin(); const { data: existingDoc } = await admin .from('documents') .select('id, content') .eq('tenant_id', g.tenantId) .eq('type', DOC_TYPE) .order('updated_at', { ascending: false }) .limit(1) .maybeSingle(); if (!existingDoc?.content) { return NextResponse.json({ error: 'No se encontraron contactos.' }, { status: 404 }); } let contacts: ClientContact[] = []; try { contacts = JSON.parse(existingDoc.content); } catch { contacts = []; } const nextContacts = contacts.filter((c) => c.id !== id); const { error: updateErr } = await admin .from('documents') .update({ content: JSON.stringify(nextContacts), updated_at: new Date().toISOString(), }) .eq('id', existingDoc.id); if (updateErr) { return NextResponse.json({ error: updateErr.message }, { status: 500 }); } return NextResponse.json({ ok: true, contacts: nextContacts }); }