import { useEffect, useRef, useState, useCallback, useMemo, useDeferredValue } from 'react' import { useParams, useNavigate } from '@tanstack/react-router' import { Loader2, BarChart3, Play, AlertTriangle, AlertCircle, PanelRightClose, PanelRightOpen, ArrowLeft, GitMerge, MousePointer2, Check, Pencil } from 'lucide-react' import { Button } from '@/components/ui/button' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip' import { Separator } from '@/components/ui/separator' import { BpmnCanvas, type BpmnElementCategory } from './BpmnCanvas' import { ActivityPanel } from './ActivityPanel' import { GatewayPanel, isXorSumValid } from './GatewayPanel' import { ResourcesPanel } from './ResourcesPanel' import { GlobalSettingsPanel } from './GlobalSettingsPanel' import { WorkspaceSkeleton } from './WorkspaceSkeleton' import { useProcessStore } from '@/store/process-store' import { useSimulationStore } from '@/store/simulation-store' import { useSimulate } from '../simulation/useSimulate' import { useLibraryStore } from '@/store/library-store' import { useAuth } from '@/auth/AuthContext' import { AppHeader } from '@/components/AppHeader' import { ErrorBoundary } from '@/components/ErrorBoundary' type SelectedCategory = 'activity' | 'gateway' | null export function WorkspacePage() { const { processId } = useParams({ from: '/workspace/$processId' }) const navigate = useNavigate() const { currentProcess, activities, isLoading, error, loadProcess, gateways, updateProcess } = useProcessStore() const { isRunning, resultActual, error: simError, selectActivity, loadLatestForProcess, lastPersistedExecutedAt } = useSimulationStore() const { simulate } = useSimulate() const { user } = useAuth() const [sidebarOpen, setSidebarOpen] = useState(true) const [selectedElementId, setSelectedElementId] = useState(null) const [selectedElementName, setSelectedElementName] = useState('') const [selectedCategory, setSelectedCategory] = useState(null) const [activeTab, setActiveTab] = useState('elemento') // Estado para edición inline del header const [editingField, setEditingField] = useState<'name' | 'client' | null>(null) const [editValue, setEditValue] = useState('') const [savedField, setSavedField] = useState<'name' | 'client' | null>(null) const savedTimerRef = useRef | null>(null) // Tags chip input const [tagInput, setTagInput] = useState('') const [showTagSuggestions, setShowTagSuggestions] = useState(false) const getAllTags = useLibraryStore((state) => state.getAllTags) useEffect(() => { loadProcess(processId) }, [processId, loadProcess]) // Cargar el último executedAt persistido para comparar contra updatedAt (indicador stale). // Depende de currentProcess?.id (no del objeto) a propósito: evita re-disparar el efecto // en cada edición de campos del proceso, solo cuando cambia de proceso. useEffect(() => { if (currentProcess) loadLatestForProcess(currentProcess.id) // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentProcess?.id, loadLatestForProcess]) // client_viewer no puede editar — redirigir al reporte useEffect(() => { if (user?.platformRole === 'client_viewer') { void navigate({ to: '/report/$processId', params: { processId } }) } }, [user, processId, navigate]) // orgName viene directamente del proceso cargado via JOIN en getById (sin fetch separado) // Validación XOR: todos los gateways exclusivos deben sumar 1.0 const xorValidation = useMemo(() => { const invalidIds = gateways .filter((gw) => gw.gatewayType === 'exclusive' && !isXorSumValid(gw)) .map((gw) => gw.bpmnElementId) return { valid: invalidIds.length === 0, invalidIds } }, [gateways]) const canSimulate = !isRunning && xorValidation.valid // Hay cambios sin simular cuando updatedAt supera al executedAt de la última simulación persistida const isStale = Boolean( currentProcess && currentProcess.updatedAt > (lastPersistedExecutedAt ?? 0) ) // IDs de actividades marcadas como automatizables — alimenta los overlays del canvas. // useDeferredValue evita re-renders en cascada mientras el usuario edita el panel. const automatableElementIds = useMemo( () => activities.filter((a) => a.automatable).map((a) => a.bpmnElementId), [activities] ) const deferredAutomatableIds = useDeferredValue(automatableElementIds) const handleElementClick = useCallback(( elementId: string, elementName: string, category: BpmnElementCategory ) => { if (category === 'other') return setSelectedElementId(elementId) setSelectedElementName(elementName) setSelectedCategory(category) selectActivity(category === 'activity' ? elementId : null) setActiveTab('elemento') if (!sidebarOpen) setSidebarOpen(true) }, [selectActivity, sidebarOpen]) function clearSelection() { setSelectedElementId(null) setSelectedElementName('') setSelectedCategory(null) selectActivity(null) } async function saveField(field: 'name' | 'client') { if (field === 'name') { const trimmed = editValue.trim() if (trimmed && trimmed !== currentProcess?.name) { await updateProcess({ name: trimmed }, user!.id) } } else { const trimmed = editValue.trim() if (trimmed !== (currentProcess?.clientName ?? '')) { await updateProcess({ clientName: trimmed }, user!.id) } } setEditingField(null) setSavedField(field) if (savedTimerRef.current) clearTimeout(savedTimerRef.current) savedTimerRef.current = setTimeout(() => setSavedField(null), 1500) } function addTag(raw: string) { const normalized = raw.trim().toLowerCase() if (!normalized) return const current = currentProcess?.tags ?? [] if (current.some((t) => t === normalized)) return updateProcess({ tags: [...current, normalized] }, user!.id) setTagInput('') setShowTagSuggestions(false) } function removeTag(tag: string) { const current = currentProcess?.tags ?? [] updateProcess({ tags: current.filter((t) => t !== tag) }, user!.id) } function handleTagKeyDown(e: React.KeyboardEvent) { if (e.key === 'Enter') { e.preventDefault(); if (tagInput.trim()) addTag(tagInput) } if (e.key === 'Escape') { setTagInput(''); setShowTagSuggestions(false) } } async function handleSimulate() { if (!canSimulate) return const simResult = await simulate() if (simResult) navigate({ to: '/report/$processId', params: { processId } }) } // isLoading manda siempre, incluso si currentProcess todavía tiene el proceso // anterior en el store (navegación directa entre dos workspaces distintos). if (isLoading || user?.platformRole === 'client_viewer') { return } if (error || !currentProcess) { return (

{error ?? 'No se pudo cargar el proceso. Revisá tu conexión.'}

) } // client_viewer ya fue redirigido antes de este punto; solo verificamos client_editor aquí const isClientRole = user?.platformRole === 'client_editor' const backLabel = isClientRole ? 'Procesos' : currentProcess.orgName ?? 'Biblioteca' function handleBack() { void navigate({ to: '/library', search: { org: currentProcess?.orgId ?? undefined }, }) } // Label del tab "elemento" según lo seleccionado const elementTabLabel = selectedCategory === 'gateway' ? 'Gateway' : 'Actividad' const elementTabIcon = selectedCategory === 'gateway' ? : return (
{/* ── Topbar ─────────────────────────────────────────────────── */}
{/* Nombre del proceso — editable inline */} {editingField === 'name' ? ( setEditValue(e.target.value)} onBlur={() => saveField('name')} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); saveField('name') } if (e.key === 'Escape') setEditingField(null) }} /> ) : (
{ setEditValue(currentProcess.name); setEditingField('name') }} >

{currentProcess.name}

{savedField === 'name' ? : }
)} {/* Área — display de solo lectura (editable desde el panel de configuración) */} {currentProcess.areaName && (

{currentProcess.areaName}

)} {/* Tags — chips removibles + input inline */} {(() => { const processTags = currentProcess.tags ?? [] const suggestions = getAllTags() .filter((t) => t.includes(tagInput.toLowerCase()) && !processTags.includes(t)) .slice(0, 6) return (
{processTags.map((tag) => ( {tag} ))}
{ setTagInput(e.target.value); setShowTagSuggestions(true) }} onKeyDown={handleTagKeyDown} onFocus={() => setShowTagSuggestions(true)} onBlur={() => setTimeout(() => setShowTagSuggestions(false), 150)} placeholder="Agregar tag..." className="text-xs bg-transparent border-b border-muted outline-none w-24 placeholder:text-muted-foreground/50" /> {showTagSuggestions && suggestions.length > 0 && (
{suggestions.map((s) => ( ))}
)}
) })()}
{/* Errores de simulación */} {simError && (
{simError}
)} {/* Advertencia de XOR inválido */} {!xorValidation.valid && (
{xorValidation.invalidIds.length} gateway{xorValidation.invalidIds.length > 1 ? 's' : ''} con probabilidades inválidas

Gateways XOR que no suman 100%:

{xorValidation.invalidIds.map((id) => (

• {id}

))}

Ajustá las probabilidades para habilitar la simulación

)} {/* Ver reporte si ya hay simulación */} {resultActual && ( )} {/* Indicador stale */} {isStale && (
Hay cambios sin simular
)} {/* Simular */} {!xorValidation.valid && ( Corregí las probabilidades de los gateways XOR primero )} {sidebarOpen ? 'Cerrar panel' : 'Abrir panel'}
{/* ── Main ───────────────────────────────────────────────────── */}
{/* Canvas BPMN */}
{/* Chip de elemento seleccionado */} {selectedElementId && (
{selectedElementName || selectedElementId}
)} {/* Hint inicial */} {!selectedElementId && (

Hacé click en una tarea o gateway para configurarlo

)}
{/* Panel lateral */}
{sidebarOpen && ( {elementTabIcon}{elementTabLabel} Recursos Global {selectedCategory === 'gateway' ? : } )}
{/* Toggle flotante cuando sidebar cerrado */} {!sidebarOpen && ( )}
) } export function WorkspacePageWithBoundary() { return ( ) }