'use client'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { CheckCircle2, Download, ExternalLink, FileText, FolderOpen, HardDrive, Loader2, Trash2, Upload, } from 'lucide-react'; import { PageHeader } from '@/components/ui'; type Asset = { id: string; title: string; folder?: string; file_name: string; mime_type?: string | null; bytes?: number | null; created_at?: string; }; type Dual = { agency: boolean; client: boolean; both: boolean; label: string; }; type DriveFile = { id: string; name: string; mimeType?: string; bytes?: number | null; modifiedTime?: string; webViewLink?: string; }; function formatBytes(n?: number | null) { if (!n) return '—'; if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; return `${(n / (1024 * 1024)).toFixed(1)} MB`; } function AssetImagePreview({ assetId, alt }: { assetId: string; alt: string }) { const [url, setUrl] = useState(null); useEffect(() => { let cancelled = false; (async () => { const r = await fetch(`/api/assets/download?id=${encodeURIComponent(assetId)}`); const j = await r.json(); if (!cancelled && r.ok && j.url) setUrl(j.url); })(); return () => { cancelled = true; }; }, [assetId]); if (!url) { return

Cargando preview…

; } return ( // eslint-disable-next-line @next/next/no-img-element {alt} ); } function iconHint(mime?: string | null) { if (!mime) return 'FILE'; if (mime.startsWith('image/')) return 'IMG'; if (mime.includes('pdf')) return 'PDF'; if (mime.includes('folder')) return 'DIR'; if (mime.includes('sheet') || mime.includes('csv') || mime.includes('excel')) return 'XLS'; if (mime.includes('zip') || mime.includes('rar')) return 'ZIP'; return 'FILE'; } export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string }) { const [tab, setTab] = useState<'portal' | 'drive'>('portal'); const [assets, setAssets] = useState([]); const [driveFiles, setDriveFiles] = useState([]); const [driveFolder, setDriveFolder] = useState(null); const [warning, setWarning] = useState(null); const [folder, setFolder] = useState(''); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); const [err, setErr] = useState(null); const [drag, setDrag] = useState(false); const [dualMap, setDualMap] = useState>({}); const [detailId, setDetailId] = useState(null); const [comments, setComments] = useState< { id: string; body: string; author_party: string; author_email?: string; created_at: string }[] >([]); const [commentText, setCommentText] = useState(''); const [party, setParty] = useState<'agency' | 'client'>('client'); const [collabBusy, setCollabBusy] = useState(false); const loadPortal = useCallback(async () => { const res = await fetch('/api/assets'); const data = await res.json(); const list: Asset[] = data.assets || []; setAssets(list); if (data.warning) setWarning(data.warning); if (list.length) { const ids = list.map((a) => a.id).join(','); const c = await fetch(`/api/assets/collab?ids=${ids}`); const cj = await c.json(); if (c.ok) { setDualMap(cj.dual || {}); if (cj.party) setParty(cj.party); } } }, []); const loadDrive = useCallback(async () => { const res = await fetch('/api/assets/drive'); const data = await res.json(); setDriveFiles(data.files || []); setDriveFolder(data.folderName || null); if (data.warning) setWarning(data.warning); else if (data.linked) setWarning(null); }, []); const load = useCallback(async () => { setLoading(true); setErr(null); if (tab === 'portal') await loadPortal(); else await loadDrive(); setLoading(false); }, [tab, loadPortal, loadDrive]); useEffect(() => { void load(); }, [load]); const folders = useMemo(() => { const s = new Set(); for (const a of assets) { if (a.folder) s.add(a.folder); } return [...s].sort(); }, [assets]); const visible = useMemo(() => { if (!folder) return assets; return assets.filter((a) => (a.folder || '') === folder); }, [assets, folder]); async function uploadFiles(fileList: FileList | File[]) { const files = [...fileList]; if (!files.length) return; setUploading(true); setErr(null); const fd = new FormData(); fd.set('folder', folder); for (const f of files) fd.append('files', f); const res = await fetch('/api/assets', { method: 'POST', body: fd }); const data = await res.json(); if (!res.ok) { setErr( data.error || (data.errors || []).join(' · ') || 'Error al subir' ); } else if (data.errors?.length) { setErr(data.errors.join(' · ')); } setTab('portal'); await loadPortal(); setUploading(false); } async function download(id: string) { const res = await fetch(`/api/assets/download?id=${encodeURIComponent(id)}`); const data = await res.json(); if (!res.ok || !data.url) { setErr(data.error || 'No se pudo descargar'); return; } window.open(data.url, '_blank', 'noopener,noreferrer'); } async function openDrive(file: DriveFile) { if (file.webViewLink) { window.open(file.webViewLink, '_blank', 'noopener,noreferrer'); return; } const res = await fetch( `/api/assets/drive?download=${encodeURIComponent(file.id)}` ); const data = await res.json(); if (!res.ok || !data.url) { setErr(data.error || 'No se pudo abrir en Drive'); return; } window.open(data.url, '_blank', 'noopener,noreferrer'); } async function remove(id: string) { if (!confirm('¿Eliminar este archivo de la biblioteca?')) return; const res = await fetch(`/api/assets?id=${encodeURIComponent(id)}`, { method: 'DELETE', }); if (!res.ok) { const data = await res.json().catch(() => ({})); setErr(data.error || 'No se pudo borrar'); } await loadPortal(); } return (
{warning && (

{warning}

)} {err &&

{err}

} {tab === 'portal' && ( <>
setFolder(e.target.value)} list="asset-folders" /> {folders.map((f) => (
{ e.preventDefault(); setDrag(true); }} onDragOver={(e) => e.preventDefault()} onDragLeave={() => setDrag(false)} onDrop={(e) => { e.preventDefault(); setDrag(false); if (e.dataTransfer.files?.length) { void uploadFiles(e.dataTransfer.files); } }} className={`rounded-xl border border-dashed px-6 py-10 text-center transition-colors ${ drag ? 'border-brand-500 bg-brand-500/10' : 'border-gray-700 bg-[#111827]' }`} >

Arrastra aquí cualquier archivo (máx. 80 MB c/u)

Se guarda en portal y se refleja en Drive si el cliente está enlazado

{folders.length > 0 && (
{folders.map((f) => ( ))}
)} {loading ? (

Cargando…

) : visible.length === 0 ? (

Sin archivos en esta vista

) : (
    {visible.map((a) => (
  • {iconHint(a.mime_type)}

    {a.file_name}

    {a.folder ? `${a.folder}/` : ''} {formatBytes(a.bytes)} {a.created_at ? ` · ${new Date(a.created_at).toLocaleString('es-ES')}` : ''}

  • ))}
)} )} {tab === 'drive' && ( <> {loading ? (

Cargando Drive…

) : driveFiles.length === 0 ? (

Sin archivos en Drive

Conecta marketing@ en Agencia y ejecuta auto-match por nombre.

) : (
    {driveFiles.map((f) => (
  • {iconHint(f.mimeType)}

    {f.name}

    {formatBytes(f.bytes)} {f.modifiedTime ? ` · ${new Date(f.modifiedTime).toLocaleString('es-ES')}` : ''}

  • ))}
)} )} {detailId && (

{assets.find((a) => a.id === detailId)?.file_name || 'Archivo'}

Doble check · {dualMap[detailId]?.label || 'En revisión'} · tú:{' '} {party === 'agency' ? 'agencia' : 'cliente'}

{(() => { const a = assets.find((x) => x.id === detailId); const isImg = a?.mime_type?.startsWith('image/'); return isImg ? ( ) : (

Preview disponible para imágenes. Usa descargar para el resto.

); })()}
    {comments.length === 0 && (
  • Sin comentarios.
  • )} {comments.map((c) => (
  • {c.author_party === 'agency' ? 'Agencia' : 'Cliente'} {c.author_email ? ` · ${c.author_email}` : ''}

    {c.body}

  • ))}
setCommentText(e.target.value)} placeholder="Comentario…" className="flex-1 rounded-lg bg-gray-900 border border-gray-700 px-3 py-2 text-xs text-white" />
)}
); }