'use client'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { CheckCircle2, Download, ExternalLink, FileText, FolderOpen, HardDrive, Loader2, Trash2, Upload, } from 'lucide-react'; type Asset = { id: string; title: string; folder?: string; file_name: string; mime_type?: string | null; bytes?: number | null; created_at?: 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 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 loadPortal = useCallback(async () => { const res = await fetch('/api/assets'); const data = await res.json(); setAssets(data.assets || []); if (data.warning) setWarning(data.warning); }, []); 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 (

Biblioteca de activos

Archivo oficial de {tenantLabel}. Subidas al portal se copian a la carpeta Drive del cliente si está enlazada.

{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')}` : ''}

    Validado
  • ))}
)} )} {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')}` : ''}

  • ))}
)} )}
); }