import { useEffect, useState, useRef, useCallback } from 'react' import { useNavigate } from '@tanstack/react-router' import { Upload, Search, Plus, Building2, FolderX, GitBranch, Clock, TrendingUp, BarChart2, FileX2, X, Settings, MoreVertical, Trash2, Share2, Lock, User, } from 'lucide-react' import { useLibraryStore } from '@/store/library-store' import { useAuth } from '@/auth/AuthContext' import { supabaseProcessRepo } from '@/persistence/supabase/process-repo' import { formatCurrency } from '@/lib/format' import type { Process, ProcessGroup, Simulation } from '@/domain/types' import { AppHeader } from '@/components/AppHeader' // ─── Helpers ────────────────────────────────────────────────────────────────── function relativeDate(ts: number): string { const days = Math.floor((Date.now() - ts) / 86_400_000) if (days === 0) return 'hoy' if (days === 1) return 'hace 1 día' return `hace ${days} días` } function tagCloudData(processes: Process[]): Array<{ tag: string; count: number; fontSize: string }> { const freq = new Map() processes.forEach((p) => p.tags.forEach((t) => freq.set(t, (freq.get(t) ?? 0) + 1))) const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12) if (sorted.length === 0) return [] const maxCount = sorted[0][1] const minCount = sorted[sorted.length - 1][1] const range = maxCount - minCount || 1 return sorted.map(([tag, count]) => { const t = (count - minCount) / range return { tag, count, fontSize: `${Math.round(10 + t * 3)}px` } }) } function matchesSearch(p: Process, q: string): boolean { const lower = q.toLowerCase() return p.name.toLowerCase().includes(lower) || p.tags.some((t) => t.includes(lower)) } function matchesTags(p: Process, activeTags: string[]): boolean { return activeTags.every((t) => p.tags.includes(t)) } // ─── Ownership Badge ────────────────────────────────────────────────────────── function OwnershipBadge({ process, currentUserId }: { process: Process; currentUserId: string }) { const isOwn = process.ownerId === currentUserId return (
{isOwn ? : }
) } // ─── Toast ──────────────────────────────────────────────────────────────────── function useToast() { const [message, setMessage] = useState(null) function show(msg: string) { setMessage(msg) setTimeout(() => setMessage(null), 2800) } return { message, show } } function Toast({ message }: { message: string | null }) { if (!message) return null return (
{message}
) } // ─── KPI Badge ──────────────────────────────────────────────────────────────── function KpiBadge({ process, simulation }: { process: Process; simulation: Simulation | null | undefined }) { if (simulation === undefined) return null // cargando if (!simulation) { return Sin simular } if (simulation.resultAutomated) { const ahorro = (simulation.result.totalCost - simulation.resultAutomated.totalCost) * process.annualFrequency return (
{formatCurrency(ahorro, process.currency)}

Ahorro anual proyectado

) } const costoAnual = simulation.result.totalCost * process.annualFrequency return (
{formatCurrency(costoAnual, process.currency)}

Costo anual actual

) } // ─── Process Card ───────────────────────────────────────────────────────────── function ProcessCard({ process, simulation, showGroupChip, groupName, onDelete, currentUserId, onShared }: { process: Process simulation: Simulation | null | undefined showGroupChip?: boolean groupName?: string onDelete?: () => void currentUserId: string onShared?: () => void }) { const navigate = useNavigate() const [menuState, setMenuState] = useState(null) const isOwn = process.ownerId === currentUserId // Cierra el menú al hacer click fuera de él useEffect(() => { if (!menuState) return function handleDocClick() { setMenuState(null) } document.addEventListener('click', handleDocClick) return () => document.removeEventListener('click', handleDocClick) }, [menuState]) return (
{ if (menuState) { setMenuState(null); return } navigate({ to: '/workspace/$processId', params: { processId: process.id } }) }} > {/* Ícono */}
{/* Info central */}

{process.name || 'Sin nombre'}

Modificado {relativeDate(process.updatedAt)}
{(process.tags.length > 0 || showGroupChip) && (
{showGroupChip && groupName && ( {groupName} )} {process.tags.map((tag) => ( {tag} ))}
)}
{/* KPI derecha */}
{/* Menú de opciones (⋮) — en el flujo flex, a la derecha del KPI */} {onDelete && (
e.stopPropagation()} > {menuState === 'menu' && (
e.stopPropagation()} > {isOwn && ( )}
)} {menuState === 'confirm' && (
e.stopPropagation()} >

¿Eliminar proceso?

Esta acción no se puede deshacer.

)}
)}
) } // ─── Group Card ─────────────────────────────────────────────────────────────── function GroupCard({ group, processCount, lastUpdated, onClick }: { group: ProcessGroup processCount: number lastUpdated: number | null onClick: () => void }) { return ( ) } // ─── New Group Card ─────────────────────────────────────────────────────────── function NewGroupCard({ label, onCreate }: { label: string; onCreate: (name: string) => void }) { const [editing, setEditing] = useState(false) const [value, setValue] = useState('') const inputRef = useRef(null) const confirm = useCallback(() => { const trimmed = value.trim() if (trimmed) onCreate(trimmed) setValue('') setEditing(false) }, [value, onCreate]) const cancel = useCallback(() => { setValue('') setEditing(false) }, []) useEffect(() => { if (editing) inputRef.current?.focus() }, [editing]) if (editing) { return (
setValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') confirm() if (e.key === 'Escape') cancel() }} placeholder={`Nombre del ${label.toLowerCase()}...`} className="w-full text-[12px] bg-transparent border-none outline-none placeholder:text-muted-foreground" />
) } return ( ) } // ─── Home View ──────────────────────────────────────────────────────────────── function HomeView({ onNavigateToGroup, onImport, ownerFilter, currentUserId, onShared, }: { onNavigateToGroup: (groupId: string | null) => void onImport: (groupId?: string) => void ownerFilter: 'all' | 'mine' currentUserId: string onShared: () => void }) { const { groups, processes, settings, createGroup, isLoading, error, load } = useLibraryStore() const { user } = useAuth() const [search, setSearch] = useState('') const [activeTags, setActiveTags] = useState([]) const visibleProcesses = ownerFilter === 'mine' ? processes.filter((p) => p.ownerId === currentUserId) : processes const cloud = tagCloudData(visibleProcesses) const unclassifiedProcesses = visibleProcesses.filter((p) => p.groupId == null) const isSearching = search.trim().length > 0 || activeTags.length > 0 const filteredProcesses = isSearching ? visibleProcesses.filter((p) => matchesSearch(p, search) && matchesTags(p, activeTags)) : [] function toggleTag(tag: string) { setActiveTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]) } // Skeleton mientras carga y aún no hay datos (evita flash de empty state) if (isLoading && groups.length === 0 && processes.length === 0) { return (
{[1, 2, 3].map((i) => (
))}
{[1, 2].map((i) => (
))}
) } // Error state: el load() falló o se colgó (timeout) — distinto de "biblioteca vacía" if (error && groups.length === 0 && processes.length === 0) { return (

No se pudo cargar la biblioteca

{error}

) } // Empty state: sin grupos ni procesos (ya terminó de cargar) if (groups.length === 0 && processes.length === 0) { return (

Tu biblioteca está vacía

Importá tu primer BPMN para empezar

) } return (
{/* Búsqueda */}
setSearch(e.target.value)} placeholder="Buscar proceso por nombre..." className="w-full pl-9 pr-4 py-2 text-sm rounded-lg border border-border bg-background focus:outline-none focus:ring-1 focus:ring-primary" />
{/* Resultados de búsqueda */} {isSearching && (
{/* Filtros activos */} {activeTags.length > 0 && (
Filtros activos: {activeTags.map((tag) => ( ))}
)}

{filteredProcesses.length} resultado{filteredProcesses.length !== 1 ? 's' : ''}

{filteredProcesses.length === 0 && (

Sin resultados para esa búsqueda.

)} {filteredProcesses.map((p) => { const group = groups.find((g) => g.id === p.groupId) return ( ) })}
)} {/* Vista normal — grupos */} {!isSearching && ( <> {/* Tag cloud (wow): visible cuando hay >= 3 tags distintos */} {cloud.length >= 3 && (
Tags: {cloud.map(({ tag, fontSize }) => ( ))}
)} {/* Tags simples cuando hay 1-2 */} {cloud.length > 0 && cloud.length < 3 && (
Tags: {cloud.map(({ tag }) => ( ))}
)} {/* Header sección grupos */}
{settings.groupLabel}s {groups.length}
{/* Grid de grupos */}
{groups.map((group) => { const groupProcesses = visibleProcesses.filter((p) => p.groupId === group.id) const lastUpdated = groupProcesses.length > 0 ? Math.max(...groupProcesses.map((p) => p.updatedAt)) : null return ( onNavigateToGroup(group.id)} /> ) })} createGroup(name, user!.id)} />
{/* Sin clasificar */} {unclassifiedProcesses.length > 0 && ( )} )}
) } // ─── ProcessCardWithSim (carga simulación lazy) ─────────────────────────────── function ProcessCardWithSim({ process, showGroupChip, groupName, currentUserId, onShared }: { process: Process showGroupChip?: boolean groupName?: string currentUserId: string onShared: () => void }) { const { getLatestSimulation, deleteProcess } = useLibraryStore() const [simulation, setSimulation] = useState(undefined) useEffect(() => { let cancelled = false getLatestSimulation(process.id).then((sim) => { if (!cancelled) setSimulation(sim ?? null) }) return () => { cancelled = true } }, [process.id, getLatestSimulation]) return ( deleteProcess(process.id)} currentUserId={currentUserId} onShared={onShared} /> ) } // ─── Group View ─────────────────────────────────────────────────────────────── function GroupView({ groupId, onBack, currentUserId, ownerFilter, onShared, }: { groupId: string | null onBack: () => void currentUserId: string ownerFilter: 'all' | 'mine' onShared: () => void }) { const { groups, processes, settings } = useLibraryStore() const [search, setSearch] = useState('') const group = groupId ? groups.find((g) => g.id === groupId) : null const groupName = group?.name ?? `Procesos sin ${settings.groupLabel.toLowerCase()}` const baseProcesses = ownerFilter === 'mine' ? processes.filter((p) => p.ownerId === currentUserId) : processes const groupProcesses = baseProcesses .filter((p) => groupId === null ? p.groupId == null : p.groupId === groupId) .filter((p) => matchesSearch(p, search)) return (
{/* Breadcrumb */}
/ {groupName}

{groupProcesses.length} proceso{groupProcesses.length !== 1 ? 's' : ''}

{/* Búsqueda */}
setSearch(e.target.value)} placeholder={`Buscar en este ${settings.groupLabel.toLowerCase()}...`} className="w-full pl-9 pr-4 py-2 text-sm rounded-lg border border-border bg-background focus:outline-none focus:ring-1 focus:ring-primary" />
{/* Empty state */} {groupProcesses.length === 0 && !search && (

No hay procesos en este {settings.groupLabel.toLowerCase()}.

Importá un BPMN para comenzar.

)} {/* Lista de procesos */}
{groupProcesses.map((p) => ( ))}
) } // ─── LibraryPage ────────────────────────────────────────────────────────────── export function LibraryPage() { const navigate = useNavigate() const { load, processes, groups, settings } = useLibraryStore() const { user } = useAuth() const [view, setView] = useState<'home' | 'group'>('home') const [activeGroupId, setActiveGroupId] = useState(null) const [transitioning, setTransitioning] = useState(false) const [ownerFilter, setOwnerFilter] = useState<'all' | 'mine'>('all') const { message: toastMessage, show: showToast } = useToast() useEffect(() => { if (!user) return // no fetchear hasta tener usuario confirmado (evita race condition con sesión Supabase) load() }, [load, user]) function navigateToGroup(groupId: string | null) { setTransitioning(true) setTimeout(() => { setActiveGroupId(groupId) setView('group') setTransitioning(false) }, 150) } function navigateHome() { setTransitioning(true) setTimeout(() => { setView('home') setActiveGroupId(null) setTransitioning(false) }, 150) } function handleImport(groupId?: string) { if (groupId) { navigate({ to: '/import', search: { groupId } }) } else { navigate({ to: '/import' }) } } const currentUserId = user!.id const totalGroups = groups.length return (
{/* Header */}

Biblioteca de procesos

{processes.length} proceso{processes.length !== 1 ? 's' : ''} · {totalGroups} {settings.groupLabel.toLowerCase()}{totalGroups !== 1 ? 's' : ''}

{/* Toggle Todos / Míos */}
{/* Contenido — sin spinner bloqueante: skeleton dentro de HomeView */}
{view === 'home' ? ( showToast('Proceso compartido con el equipo')} /> ) : ( showToast('Proceso compartido con el equipo')} /> )}
) }