diff --git a/src/features/library/LibraryPage.tsx b/src/features/library/LibraryPage.tsx index a7150a0..cae952b 100644 --- a/src/features/library/LibraryPage.tsx +++ b/src/features/library/LibraryPage.tsx @@ -1,15 +1,16 @@ -import { useEffect, useState, useRef, useCallback } from 'react' -import { useNavigate, useRouterState } from '@tanstack/react-router' +import { useEffect, useState, useMemo } from 'react' +import { useNavigate } from '@tanstack/react-router' import { - Upload, Search, Plus, Building2, FolderX, GitBranch, + Upload, Search, Building2, GitBranch, Clock, TrendingUp, BarChart2, FileX2, X, Settings, MoreVertical, Trash2, Share2, Lock, User, + ChevronDown, FolderOpen, } from 'lucide-react' import { useLibraryStore } from '@/store/library-store' import { useAuth } from '@/auth/AuthContext' import { supabaseProcessRepo, type ProcessWithUserNames } from '@/persistence/supabase/process-repo' import { formatCurrency, formatSimulationTimestamp } from '@/lib/format' -import type { Process, ProcessGroup, Simulation } from '@/domain/types' +import type { Process, Simulation } from '@/domain/types' import { AppHeader } from '@/components/AppHeader' import { AppFooter } from '@/components/AppFooter' @@ -267,129 +268,22 @@ function ProcessCard({ process, simulation, showGroupChip, groupName, onDelete, ) } -// ─── 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, currentUserName, onShared, }: { - onNavigateToGroup: (groupId: string | null) => void - onImport: (groupId?: string) => void + onImport: () => void ownerFilter: 'all' | 'mine' currentUserId: string currentUserName: string onShared: () => void }) { - const { groups, processes, settings, createGroup, isLoading, error, load } = useLibraryStore() + const { groups, processes, isLoading, error, load } = useLibraryStore() const { user } = useAuth() const [search, setSearch] = useState('') const [activeTags, setActiveTags] = useState([]) @@ -399,10 +293,46 @@ function HomeView({ : processes const cloud = tagCloudData(visibleProcesses) - const unclassifiedProcesses = visibleProcesses.filter((p) => p.groupId == null) const isSearching = search.trim().length > 0 || activeTags.length > 0 + const [collapsedOrgs, setCollapsedOrgs] = useState>(new Set()) + + const orgGroups = useMemo(() => { + const byOrg = new Map() + for (const p of visibleProcesses) { + const key = p.orgId ?? '__sin_org' + const orgName = p.orgName ?? 'Sin organización' + if (!byOrg.has(key)) byOrg.set(key, { orgId: key, orgName, processes: [] }) + byOrg.get(key)!.processes.push(p) + } + return [...byOrg.values()].sort((a, b) => a.orgName.localeCompare(b.orgName)) + }, [visibleProcesses]) + + function getAreaGroups(orgProcesses: ProcessWithUserNames[]) { + const byArea = new Map() + for (const p of orgProcesses) { + const key = p.areaId ?? '__sin_area' + const areaName = p.areaName ?? 'Sin área' + if (!byArea.has(key)) byArea.set(key, { areaId: key, areaName, processes: [] }) + byArea.get(key)!.processes.push(p) + } + return [...byArea.values()].sort((a, b) => { + if (a.areaId === '__sin_area') return 1 + if (b.areaId === '__sin_area') return -1 + return a.areaName.localeCompare(b.areaName) + }) + } + + function toggleOrg(orgId: string) { + setCollapsedOrgs(prev => { + const next = new Set(prev) + if (next.has(orgId)) next.delete(orgId) + else next.add(orgId) + return next + }) + } + const filteredProcesses = isSearching ? visibleProcesses.filter((p) => matchesSearch(p, search) && matchesTags(p, activeTags)) : [] @@ -522,26 +452,23 @@ function HomeView({ {filteredProcesses.length === 0 && (

Sin resultados para esa búsqueda.

)} - {filteredProcesses.map((p) => { - const group = groups.find((g) => g.id === p.groupId) - return ( - - ) - })} + {filteredProcesses.map((p) => ( + + ))} )} - {/* Vista normal — grupos (solo roles internos) */} + {/* Vista normal — org → área (solo roles internos) */} {!isSearching && !isClientRole && ( - <> +
{/* Tag cloud (wow): visible cuando hay >= 3 tags distintos */} {cloud.length >= 3 && (
@@ -575,59 +502,89 @@ function HomeView({
)} - {/* Header sección grupos */} -
- - {settings.groupLabel}s - - - {groups.length} - -
+ {/* Grupos org → área */} + {orgGroups.map(({ orgId, orgName, processes: orgProcesses }) => { + const isCollapsed = collapsedOrgs.has(orgId) + const areaGroups = getAreaGroups(orgProcesses) - {/* 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)} + return ( +
+ {/* Header de org — colapsable */} +
- ) - })} - createGroup(name, user!.id)} - /> -
+ - {/* Sin clasificar */} - {unclassifiedProcesses.length > 0 && ( - + ) + })} + + {/* Empty state: admin sin procesos */} + {orgGroups.length === 0 && ( +
+ +

+ Tu biblioteca está vacía +

+

+ Importá tu primer BPMN para empezar +

+ +
)} - +
)} {/* Vista cliente: lista plana de procesos (sin grupos, RLS garantiza que solo ven los suyos) */} @@ -774,10 +731,8 @@ function GroupView({ export function LibraryPage() { const navigate = useNavigate() - const { load, processes, groups, settings, isLoading } = useLibraryStore() + const { load, processes } = useLibraryStore() const { user, isProfileLoaded } = useAuth() - const searchStr = useRouterState({ select: (s) => s.location.search }) - const orgParam = new URLSearchParams(searchStr).get('org') const [view, setView] = useState<'home' | 'group'>('home') const [activeGroupId, setActiveGroupId] = useState(null) const [transitioning, setTransitioning] = useState(false) @@ -789,25 +744,6 @@ export function LibraryPage() { load() }, [load, user?.id]) - // Cuando la biblioteca cargó con ?org=, hacer scroll al grupo que contiene ese org - useEffect(() => { - if (!orgParam || isLoading || processes.length === 0) return - const proc = processes.find((p) => p.orgId === orgParam) - if (!proc?.groupId) return - requestAnimationFrame(() => { - document.getElementById(`group-${proc.groupId}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }) - }) - }, [orgParam, isLoading, processes]) - - function navigateToGroup(groupId: string | null) { - setTransitioning(true) - setTimeout(() => { - setActiveGroupId(groupId) - setView('group') - setTransitioning(false) - }, 150) - } - function navigateHome() { setTransitioning(true) setTimeout(() => { @@ -817,12 +753,8 @@ export function LibraryPage() { }, 150) } - function handleImport(groupId?: string) { - if (groupId) { - navigate({ to: '/import', search: { groupId } }) - } else { - navigate({ to: '/import' }) - } + function handleImport() { + navigate({ to: '/import' }) } // Esperar a que syncProfileFromDB complete antes de renderizar UI rol-dependiente. @@ -848,8 +780,8 @@ export function LibraryPage() { const currentUserId = user.id const currentUserName = user.name - const totalGroups = groups.length const isClientRole = user.platformRole === 'client_editor' || user.platformRole === 'client_viewer' + const orgCount = new Set(processes.map(p => p.orgId).filter(Boolean)).size return (
@@ -860,7 +792,10 @@ export function LibraryPage() {

Biblioteca de procesos

- {processes.length} proceso{processes.length !== 1 ? 's' : ''} · {totalGroups} {settings.groupLabel.toLowerCase()}{totalGroups !== 1 ? 's' : ''} + {isClientRole + ? `${processes.length} proceso${processes.length !== 1 ? 's' : ''}` + : `${processes.length} proceso${processes.length !== 1 ? 's' : ''} · ${orgCount} cliente${orgCount !== 1 ? 's' : ''}` + }

@@ -888,16 +823,18 @@ export function LibraryPage() {
- {!isClientRole && ( + )} + {!isClientRole && ( +