'use client'; import React, { useEffect, useMemo, useState } from 'react'; import { ImagePlus, Plus, Trash2, Upload, Instagram, ExternalLink, X, Eye, Layers, ChevronLeft, ChevronRight, Filter, } from 'lucide-react'; import { PageHeader, Surface } from '@/components/ui'; type PipelineItem = { id: string; external_ref: string | null; title: string; format: 'post' | 'story' | 'reel' | 'carrusel'; month: string | null; status: string; cover_path: string | null; cover_mime: string | null; slide_paths: string[]; caption: string | null; notes: string | null; approved_at: string | null; created_at: string; updated_at: string; }; const STATUS_LABEL: Record = { propuesta: 'Propuesta', en_diseno: 'En diseño', revision_interna: 'Revisión interna', aprobado: 'Aprobado', rechazado: 'Rechazado', publicado: 'Publicado', }; const STATUS_COLOR: Record = { propuesta: 'bg-gray-100 text-gray-700 border-gray-200', en_diseno: 'bg-blue-100 text-blue-700 border-blue-200', revision_interna: 'bg-amber-100 text-amber-800 border-amber-200', aprobado: 'bg-emerald-100 text-emerald-700 border-emerald-200', rechazado: 'bg-red-100 text-red-700 border-red-200', publicado: 'bg-purple-100 text-purple-700 border-purple-200', }; const FORMAT_CONFIG: Record = { post: { label: 'Post', badgeCls: 'bg-blue-50 text-blue-700 border-blue-200' }, carrusel: { label: 'Carrusel', badgeCls: 'bg-indigo-50 text-indigo-700 border-indigo-200' }, reel: { label: 'Reel', badgeCls: 'bg-pink-50 text-pink-700 border-pink-200' }, story: { label: 'Story', badgeCls: 'bg-amber-50 text-amber-700 border-amber-200' }, }; const AGENCY_STATUS_ORDER = [ 'propuesta', 'en_diseno', 'revision_interna', 'aprobado', 'rechazado', 'publicado', ]; function mediaUrl(path: string | null) { if (!path) return null; return `/api/creative-pipeline/media?path=${encodeURIComponent(path)}`; } function monthLabel(m: string | null) { if (!m) return 'Sin mes'; const [y, mm] = m.split('-'); const d = new Date(Number(y), Number(mm) - 1, 1); return d.toLocaleDateString('es-ES', { month: 'long', year: 'numeric' }); } function monthShortLabel(m: string | null) { if (!m) return 'Sin mes'; const [y, mm] = m.split('-'); const d = new Date(Number(y), Number(mm) - 1, 1); const name = d.toLocaleDateString('es-ES', { month: 'short' }); return `${name} ${y.slice(2)}`; } function getInstagramUrl(notes: string | null): string | null { if (!notes) return null; const match = notes.match(/https?:\/\/(?:www\.)?instagram\.com\S+/i); return match ? match[0] : null; } export default function CreativePipelineBoard() { const [items, setItems] = useState([]); const [isAgency, setIsAgency] = useState(false); const [loading, setLoading] = useState(true); const [warning, setWarning] = useState(null); const [err, setErr] = useState(null); const [busyId, setBusyId] = useState(null); const [showForm, setShowForm] = useState(false); const [newTitle, setNewTitle] = useState(''); const [newFormat, setNewFormat] = useState('post'); const [newMonth, setNewMonth] = useState(''); const [selectedMonth, setSelectedMonth] = useState('all'); const [selectedFormat, setSelectedFormat] = useState('all'); const [previewItem, setPreviewItem] = useState(null); async function load() { setLoading(true); try { const res = await fetch('/api/creative-pipeline'); const data = await res.json(); setItems(data.items ?? []); setIsAgency(Boolean(data.isAgency)); setWarning(data.warning ?? null); } finally { setLoading(false); } } useEffect(() => { void load(); }, []); const monthCounts = useMemo(() => { const map = new Map(); for (const it of items) { const key = it.month || 'sin-mes'; map.set(key, (map.get(key) || 0) + 1); } return map; }, [items]); const months = useMemo(() => { const set = new Set(); for (const it of items) { if (it.month) set.add(it.month); } return Array.from(set).sort(); }, [items]); const filteredItems = useMemo(() => { return items.filter((it) => { if (selectedMonth !== 'all' && (it.month || 'sin-mes') !== selectedMonth) return false; if (selectedFormat !== 'all' && it.format !== selectedFormat) return false; return true; }); }, [items, selectedMonth, selectedFormat]); const grouped = useMemo(() => { const map = new Map(); for (const it of filteredItems) { const key = it.month || 'sin-mes'; if (!map.has(key)) map.set(key, []); map.get(key)!.push(it); } return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b)); }, [filteredItems]); async function createItem(e: React.FormEvent) { e.preventDefault(); if (!newTitle.trim()) return; setErr(null); const res = await fetch('/api/creative-pipeline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newTitle, format: newFormat, month: newMonth || null }), }); const data = await res.json(); if (!res.ok) { setErr(data.error || 'Error al crear'); return; } setNewTitle(''); setNewMonth(''); setShowForm(false); await load(); } async function setStatus(id: string, status: string) { setBusyId(id); setErr(null); try { const res = await fetch('/api/creative-pipeline', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, status }), }); const data = await res.json(); if (!res.ok) { setErr(data.error || 'Error al cambiar estado'); return; } if (previewItem && previewItem.id === id) { setPreviewItem({ ...previewItem, status }); } await load(); } finally { setBusyId(null); } } async function removeItem(id: string) { setBusyId(id); try { await fetch(`/api/creative-pipeline?id=${encodeURIComponent(id)}`, { method: 'DELETE' }); if (previewItem?.id === id) setPreviewItem(null); await load(); } finally { setBusyId(null); } } async function uploadImage( itemId: string, file: File, slot: 'cover' | 'slide', index = 0 ) { setBusyId(itemId); setErr(null); try { const fd = new FormData(); fd.append('file', file); fd.append('itemId', itemId); fd.append('slot', slot); fd.append('index', String(index)); const res = await fetch('/api/creative-pipeline/upload', { method: 'POST', body: fd }); const data = await res.json(); if (!res.ok) { setErr(data.error || 'Error al subir imagen'); return; } await load(); } finally { setBusyId(null); } } return (
setShowForm((v) => !v)} className="inline-flex items-center gap-1.5 rounded-lg bg-brand-600 px-3 py-2 text-sm font-medium text-white hover:bg-brand-700" > Nueva pieza ) : null } /> {warning ? (
{warning}
) : null} {err ? (
{err}
) : null} {/* Barra de Filtros por Mes y Formato */}
Meses ({items.length} piezas):
Mostrando {filteredItems.length} de {items.length}
{/* Meses pills */}
{months.map((m) => { const count = monthCounts.get(m) || 0; const active = selectedMonth === m; return ( ); })}
{/* Formatos pills */}
Formato: {['all', 'carrusel', 'reel', 'post', 'story'].map((f) => { const active = selectedFormat === f; const label = f === 'all' ? 'Todos los formatos' : FORMAT_CONFIG[f]?.label || f; return ( ); })}
{showForm && isAgency ? (
setNewTitle(e.target.value)} className="mt-1 rounded-lg border border-gray-200 px-3 py-2 text-sm" placeholder="Certifica tu nivel" />
setNewMonth(e.target.value)} className="mt-1 rounded-lg border border-gray-200 px-3 py-2 text-sm" />
) : null} {loading ? (

Cargando creatividades…

) : items.length === 0 ? (

Todavía no hay piezas en el pipeline.

) : filteredItems.length === 0 ? (

No hay piezas con los filtros seleccionados (Mes: {selectedMonth}, Formato: {selectedFormat}).

) : (
{grouped.map(([month, monthItems]) => (

{monthLabel(month === 'sin-mes' ? null : month)}

{monthItems.length} {monthItems.length === 1 ? 'pieza' : 'piezas'}
{monthItems.map((it) => ( setStatus(it.id, s)} onDelete={() => removeItem(it.id)} onUpload={(file, slot, index) => uploadImage(it.id, file, slot, index)} onPreview={() => setPreviewItem(it)} /> ))}
))}
)} {/* Modal de Detalle / Preview */} {previewItem && ( setPreviewItem(null)} onStatus={(s) => setStatus(previewItem.id, s)} /> )}
); } function PieceCard({ item, isAgency, busy, onStatus, onDelete, onUpload, onPreview, }: { item: PipelineItem; isAgency: boolean; busy: boolean; onStatus: (status: string) => void; onDelete: () => void; onUpload: (file: File, slot: 'cover' | 'slide', index: number) => void; onPreview: () => void; }) { const cover = mediaUrl(item.cover_path); const slides = item.slide_paths || []; const isCarrusel = item.format === 'carrusel'; const formatInfo = FORMAT_CONFIG[item.format] || { label: item.format, badgeCls: 'bg-gray-100 text-gray-700 border-gray-200', }; const igUrl = getInstagramUrl(item.notes); const clientCanDecide = !isAgency && item.status === 'revision_interna'; return ( {/* Portada / Imagen */}
{cover ? ( // eslint-disable-next-line @next/next/no-img-element {item.title} ) : (
)} {/* Badges superiores */}
{isCarrusel && } {formatInfo.label} {isCarrusel && slides.length > 0 && ( {slides.length} slides )}
{/* Botón de vista rápida */} {/* Botón subir imagen agencia */} {isAgency ? ( ) : null}
{/* Tira de slides para carruseles */} {isCarrusel ? (
{Array.from({ length: Math.min(Math.max(slides.length, 1), 4) }).map((_, i) => { const url = mediaUrl(slides[i] || null); return (
{url ? ( // eslint-disable-next-line @next/next/no-img-element {`slide ) : null} {isAgency ? ( ) : null}
); })}
) : null} {/* Contenido de la tarjeta */}

{item.title}

{isAgency ? ( ) : null}
{STATUS_LABEL[item.status] || item.status} {igUrl && ( IG )}
{isAgency ? ( ) : clientCanDecide ? (
) : (

{item.status === 'aprobado' ? 'Aprobada por ti' : item.status === 'publicado' ? 'Publicado en redes' : 'Pendiente del equipo de 4Ventures'}

)}
); } function DetailModal({ item, isAgency, busy, onClose, onStatus, }: { item: PipelineItem; isAgency: boolean; busy: boolean; onClose: () => void; onStatus: (status: string) => void; }) { const slides = item.slide_paths || []; const allImages = [item.cover_path, ...slides].filter(Boolean) as string[]; const [activeIdx, setActiveIdx] = useState(0); const activePath = allImages[activeIdx] || item.cover_path; const currentUrl = mediaUrl(activePath); const igUrl = getInstagramUrl(item.notes); const formatInfo = FORMAT_CONFIG[item.format] || { label: item.format, badgeCls: 'bg-gray-100 text-gray-700 border-gray-200', }; const clientCanDecide = !isAgency && item.status === 'revision_interna'; return (
e.stopPropagation()} > {/* Lado izquierdo: visor de imagen / slider */}
{currentUrl ? ( // eslint-disable-next-line @next/next/no-img-element {item.title} ) : (
Sin imagen cargada
)} {allImages.length > 1 && ( <>
{activeIdx + 1} / {allImages.length}
)}
{/* Miniaturas de slides */} {allImages.length > 1 && (
{allImages.map((path, idx) => { const url = mediaUrl(path); const isSelected = idx === activeIdx; return ( ); })}
)}
{/* Lado derecho: información, caption y acciones */}
{formatInfo.label} {STATUS_LABEL[item.status] || item.status}

{item.title}

{monthLabel(item.month)} {item.external_ref ? `· Ref: ${item.external_ref}` : ''}

{igUrl && ( Ver publicación original en Instagram )} {item.caption && (

Texto / Copy

{item.caption}
)}
{isAgency ? (
) : clientCanDecide ? (

Tu decisión como cliente:

) : (

{item.status === 'aprobado' ? '✓ Esta pieza fue aprobada por el cliente.' : item.status === 'publicado' ? '✓ Esta pieza está publicada y archivada.' : 'En proceso con el equipo de 4Ventures.'}

)}
); }