import { useEffect, useMemo, useState } from 'react' import { Info, AlertTriangle, RotateCcw, Check, GitMerge } from 'lucide-react' import { Label } from '@/components/ui/label' import { Button } from '@/components/ui/button' import { Slider } from '@/components/ui/slider' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { ScrollArea } from '@/components/ui/scroll-area' import { useProcessStore } from '@/store/process-store' import { parseBpmnXml } from '@/domain/bpmn-parser' import { formatPercent } from '@/lib/format' import type { GatewayConfig } from '@/domain/types' interface GatewayPanelProps { selectedElementId: string | null } // Tolerancia para validar que la suma de probs XOR = 1.0 const XOR_SUM_TOLERANCE = 0.005 function sumBranches(gw: GatewayConfig): number { return gw.branches.reduce((s, b) => s + b.probability, 0) } export function isXorSumValid(gw: GatewayConfig): boolean { if (gw.gatewayType !== 'exclusive') return true return Math.abs(sumBranches(gw) - 1.0) <= XOR_SUM_TOLERANCE } export function GatewayPanel({ selectedElementId }: GatewayPanelProps) { const { gateways, currentProcess, updateGateway, activities } = useProcessStore() const [localGw, setLocalGw] = useState(null) const [isDirty, setIsDirty] = useState(false) // Mapa bpmnElementId → nombre legible (parseamos el XML una sola vez) const elementNames = useMemo(() => { if (!currentProcess) return new Map() try { const graph = parseBpmnXml(currentProcess.bpmnXml) return new Map(Array.from(graph.nodes.values()).map((n) => [n.id, n.name || n.id])) } catch { return new Map() } }, [currentProcess?.bpmnXml]) const gateway = gateways.find((g) => g.bpmnElementId === selectedElementId) ?? null useEffect(() => { setLocalGw(gateway ? structuredClone(gateway) : null) setIsDirty(false) }, [selectedElementId, gateways]) function targetLabel(targetId: string): string { // Primero buscar en actividades (tienen nombre más descriptivo) const act = activities.find((a) => a.bpmnElementId === targetId) if (act?.name) return act.name // Luego en el mapa del grafo const fromGraph = elementNames.get(targetId) if (fromGraph) return fromGraph return targetId } function setProbability(branchIndex: number, value: number) { if (!localGw) return const updated = structuredClone(localGw) updated.branches[branchIndex].probability = value setLocalGw(updated) setIsDirty(true) } function distributeUniformly() { if (!localGw) return const n = localGw.branches.length const equalProb = parseFloat((1 / n).toFixed(4)) const updated = structuredClone(localGw) updated.branches = updated.branches.map((b, i) => ({ ...b, probability: i === n - 1 ? parseFloat((1 - equalProb * (n - 1)).toFixed(4)) : equalProb, })) setLocalGw(updated) setIsDirty(true) } async function handleSave() { if (!localGw) return await updateGateway(localGw) setIsDirty(false) } // ── Empty state ────────────────────────────────────────────────────────── if (!selectedElementId) { return (

Seleccioná un gateway

Hacé click sobre un gateway en el diagrama para configurar sus probabilidades

) } if (!localGw) { return (

Gateway sin configuración

) } // ── AND Paralelo: informativo, sin sliders ──────────────────────────────── if (localGw.gatewayType === 'parallel') { return (
Gateway Paralelo (AND)

{localGw.bpmnElementId}

Ejecución paralela

En un gateway AND todas las salidas se ejecutan simultáneamente. No hay probabilidades configurables — la probabilidad de cada rama es igual a la probabilidad de llegada al gateway.

{localGw.branches.map((branch) => (

{targetLabel(branch.targetElementId)}

{branch.flowId}

100%
))}
) } // ── XOR Exclusivo / OR Inclusivo (con aviso) ────────────────────────────── const isInclusive = localGw.gatewayType === 'inclusive' const currentSum = sumBranches(localGw) const xorValid = isInclusive || Math.abs(currentSum - 1.0) <= XOR_SUM_TOLERANCE const sumColor = xorValid ? 'text-green-600' : 'text-destructive' return (
{/* Header */}
{isInclusive ? 'Gateway Inclusivo (OR)' : 'Gateway Exclusivo (XOR)'}

{localGw.bpmnElementId}

{/* Aviso para OR inclusivo */} {isInclusive && (

Gateway Inclusivo (OR) — En el MVP se simula como XOR: solo una rama se toma por instancia. Las probabilidades deben sumar 1. El soporte OR nativo llega en V1.0.

)} {/* Suma actual + botón distribuir */}
Suma de probabilidades: {formatPercent(currentSum * 100, 1)} {!xorValid && ( La suma debe ser exactamente 100% para poder simular )} {xorValid && ( Suma 100% — listo para simular )}
Reparte el 100% en partes iguales entre todas las ramas del gateway
{/* Sliders por rama */}
{localGw.branches.map((branch, idx) => (

{targetLabel(branch.targetElementId)}

{branch.flowId}

{formatPercent(branch.probability * 100, 1)}
setProbability(idx, parseFloat((v / 100).toFixed(4)))} />
))}
{/* Error de suma — banner prominente */} {!xorValid && (

La suma es {formatPercent(currentSum * 100, 1)} — debe ser 100%. Ajustá los sliders o usá "Distribuir" para repartir uniformemente. El botón Simular quedará deshabilitado hasta corregirlo.

)}
) }