feat: periodo global, métricas negocio, seguimiento notas, Drive/Social/Orgánico
Selector de fechas en header (con Personalizado) compartido en el dashboard; Métricas con Ventas/CAC/ROAS CRM, cruce canal y gráficas diarias; Seguimiento solo notas + sync Superhuman; Activos con navegación Drive; Social con métricas IG/FB; Orgánico con prioridades SEO/GEO expertas. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -38,6 +38,7 @@ type DriveFile = {
|
||||
bytes?: number | null;
|
||||
modifiedTime?: string;
|
||||
webViewLink?: string;
|
||||
isFolder?: boolean;
|
||||
};
|
||||
|
||||
function formatBytes(n?: number | null) {
|
||||
@@ -89,6 +90,10 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
const [assets, setAssets] = useState<Asset[]>([]);
|
||||
const [driveFiles, setDriveFiles] = useState<DriveFile[]>([]);
|
||||
const [driveFolder, setDriveFolder] = useState<string | null>(null);
|
||||
const [driveOpenUrl, setDriveOpenUrl] = useState<string | null>(null);
|
||||
const [drivePath, setDrivePath] = useState<
|
||||
{ id: string; name: string }[]
|
||||
>([]);
|
||||
const [warning, setWarning] = useState<string | null>(null);
|
||||
const [folder, setFolder] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -121,11 +126,15 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadDrive = useCallback(async () => {
|
||||
const res = await fetch('/api/assets/drive');
|
||||
const loadDrive = useCallback(async (folderId?: string) => {
|
||||
const qs = folderId
|
||||
? `?folderId=${encodeURIComponent(folderId)}`
|
||||
: '';
|
||||
const res = await fetch(`/api/assets/drive${qs}`);
|
||||
const data = await res.json();
|
||||
setDriveFiles(data.files || []);
|
||||
setDriveFolder(data.folderName || null);
|
||||
setDriveOpenUrl(data.driveOpenUrl || null);
|
||||
if (data.warning) setWarning(data.warning);
|
||||
else if (data.linked) setWarning(null);
|
||||
}, []);
|
||||
@@ -134,9 +143,12 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
setLoading(true);
|
||||
setErr(null);
|
||||
if (tab === 'portal') await loadPortal();
|
||||
else await loadDrive();
|
||||
else {
|
||||
const current = drivePath[drivePath.length - 1]?.id;
|
||||
await loadDrive(current);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [tab, loadPortal, loadDrive]);
|
||||
}, [tab, loadPortal, loadDrive, drivePath]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
@@ -190,6 +202,13 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
}
|
||||
|
||||
async function openDrive(file: DriveFile) {
|
||||
if (file.isFolder || file.mimeType === 'application/vnd.google-apps.folder') {
|
||||
setDrivePath((prev) => [...prev, { id: file.id, name: file.name }]);
|
||||
setLoading(true);
|
||||
await loadDrive(file.id);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (file.webViewLink) {
|
||||
window.open(file.webViewLink, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
@@ -205,6 +224,21 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
window.open(data.url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
async function goDriveRoot() {
|
||||
setDrivePath([]);
|
||||
setLoading(true);
|
||||
await loadDrive();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function goDriveCrumb(index: number) {
|
||||
const next = drivePath.slice(0, index + 1);
|
||||
setDrivePath(next);
|
||||
setLoading(true);
|
||||
await loadDrive(next[next.length - 1]?.id);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function remove(id: string) {
|
||||
if (!confirm('¿Eliminar este archivo de la biblioteca?')) return;
|
||||
const res = await fetch(`/api/assets?id=${encodeURIComponent(id)}`, {
|
||||
@@ -222,7 +256,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
<PageHeader
|
||||
eyebrow="Contenido"
|
||||
title="Activos"
|
||||
description={`Contenido de Drive de ${tenantLabel}. Pestaña Drive por defecto; Portal son archivos subidos al Centro de Mando.`}
|
||||
description={`Réplica de la carpeta Drive del cliente (${tenantLabel}) bajo Clientes. Navega carpetas como en Drive; «Abrir en Drive» para la UI nativa.`}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
@@ -462,49 +496,88 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
|
||||
{tab === 'drive' && (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-gray-600">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void goDriveRoot()}
|
||||
className="font-semibold text-brand-600 hover:underline"
|
||||
>
|
||||
{driveFolder || 'Carpeta cliente'}
|
||||
</button>
|
||||
{drivePath.map((p, i) => (
|
||||
<span key={p.id} className="inline-flex items-center gap-2">
|
||||
<span className="text-gray-400">/</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void goDriveCrumb(i)}
|
||||
className="hover:underline text-gray-800"
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
{driveOpenUrl && (
|
||||
<a
|
||||
href={driveOpenUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ml-auto inline-flex items-center gap-1 text-brand-600 hover:underline"
|
||||
>
|
||||
Abrir en Google Drive <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
Misma carpeta enlazada en Agencia (Clientes → {driveFolder || '…'}).
|
||||
Embed iframe no aplica a carpetas privadas; listamos el contenido vía API.
|
||||
</p>
|
||||
{loading ? (
|
||||
<p className="text-sm text-gray-500">Cargando Drive…</p>
|
||||
) : driveFiles.length === 0 ? (
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-12 text-center text-gray-400">
|
||||
<HardDrive className="w-10 h-10 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-base font-medium text-gray-600">
|
||||
Sin archivos en Drive
|
||||
Carpeta vacía o sin enlace
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Conecta marketing@ en Agencia y ejecuta auto-match por nombre.
|
||||
Conecta marketing@ en Agencia y ejecuta auto-match por nombre bajo
|
||||
«Clientes».
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{driveFiles.map((f) => (
|
||||
<li
|
||||
key={f.id}
|
||||
className="flex items-center gap-3 rounded-xl border border-gray-200 bg-white px-4 py-3"
|
||||
>
|
||||
<span className="text-[10px] font-bold tracking-wider text-gray-500 w-10">
|
||||
{iconHint(f.mimeType)}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-gray-900 font-medium truncate">
|
||||
{f.name}
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
{formatBytes(f.bytes)}
|
||||
{f.modifiedTime
|
||||
? ` · ${new Date(f.modifiedTime).toLocaleString('es-ES')}`
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void openDrive(f)}
|
||||
className="p-2 rounded-lg text-gray-600 hover:text-gray-900 hover:bg-gray-50"
|
||||
title="Abrir en Drive"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{driveFiles.map((f) => {
|
||||
const folder =
|
||||
f.isFolder ||
|
||||
f.mimeType === 'application/vnd.google-apps.folder';
|
||||
return (
|
||||
<li key={f.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void openDrive(f)}
|
||||
className="w-full flex items-center gap-3 rounded-xl border border-gray-200 bg-white px-4 py-3 text-left hover:border-brand-300 hover:bg-brand-50/40"
|
||||
>
|
||||
<span className="text-[10px] font-bold tracking-wider text-gray-500 w-10">
|
||||
{folder ? 'DIR' : iconHint(f.mimeType)}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-gray-900 font-medium truncate">
|
||||
{f.name}
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
{folder
|
||||
? 'Carpeta'
|
||||
: formatBytes(f.bytes)}
|
||||
{f.modifiedTime
|
||||
? ` · ${new Date(f.modifiedTime).toLocaleString('es-ES')}`
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
<ExternalLink className="w-4 h-4 text-gray-400 shrink-0" />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user