import { useEffect, useRef, useCallback } from 'react' import BpmnViewer from 'bpmn-js/lib/Viewer' export type BpmnElementCategory = 'activity' | 'gateway' | 'other' const ACTIVITY_TYPES = new Set([ 'bpmn:Task', 'bpmn:UserTask', 'bpmn:ServiceTask', 'bpmn:ScriptTask', 'bpmn:ManualTask', 'bpmn:BusinessRuleTask', 'bpmn:SubProcess', ]) const GATEWAY_TYPES = new Set([ 'bpmn:ExclusiveGateway', 'bpmn:ParallelGateway', 'bpmn:InclusiveGateway', 'bpmn:EventBasedGateway', ]) function classifyElement(type: string): BpmnElementCategory { if (ACTIVITY_TYPES.has(type)) return 'activity' if (GATEWAY_TYPES.has(type)) return 'gateway' return 'other' } // SVG inline del ícono Bot de Lucide (12×12 px, color ámbar #f59e0b). // Copiado de lucide.dev/icons/bot — robot con antena, cuerpo rectangular y ojos. const BOT_SVG = ` ` // HTML del badge de automatización. // Fondo #fffbeb (amber-50): identidad cromática con el ícono Bot ámbar, // contrasta con el fondo blanco de los nodos BPMN. function makeBadgeHtml(): string { return `
${BOT_SVG}
` } // Identificador del tipo de overlay para poder remover solo los nuestros. const OVERLAY_TYPE = 'automation-indicator' interface BpmnCanvasProps { xml: string onElementClick?: (elementId: string, elementName: string, category: BpmnElementCategory) => void selectedElementId?: string | null automatableElementIds?: string[] // IDs de actividades marcadas como automatizables className?: string } export function BpmnCanvas({ xml, onElementClick, selectedElementId, automatableElementIds = [], className, }: BpmnCanvasProps) { const containerRef = useRef(null) const viewerRef = useRef | null>(null) const isImportedRef = useRef(false) // ─── Inicializar viewer ──────────────────────────────────────────────────── useEffect(() => { if (!containerRef.current) return const viewer = new BpmnViewer({ container: containerRef.current }) viewerRef.current = viewer return () => { isImportedRef.current = false viewer.destroy() viewerRef.current = null } }, []) // ─── Sincronizar overlays de automatización ─────────────────────────────── const syncAutomationOverlays = useCallback(() => { const viewer = viewerRef.current if (!viewer || !isImportedRef.current) return const overlays = viewer.get('overlays') as any const reg = viewer.get('elementRegistry') as any // Remover overlays anteriores del tipo propio antes de re-agregar overlays.remove({ type: OVERLAY_TYPE }) for (const elementId of automatableElementIds) { try { // Leer el ancho real del elemento para posicionar el badge en la // esquina superior derecha con overlap: left = width - 12 pone el // borde derecho del badge 8px fuera del borde derecho del nodo. // (right: -8 en la API tiene semántica distinta: left = -right + width // con -8 daría left = 8 + width, enteramente fuera del nodo.) const el = reg?.get(elementId) const width = el?.width ?? 100 overlays.add(elementId, OVERLAY_TYPE, { position: { top: -8, left: width - 12 }, html: makeBadgeHtml(), }) } catch { // El elemento puede no existir en el diagrama actual — ignorar } } }, [automatableElementIds]) // ─── Importar XML ────────────────────────────────────────────────────────── useEffect(() => { const viewer = viewerRef.current if (!viewer || !xml) return isImportedRef.current = false viewer.importXML(xml).then(({ warnings }) => { if (warnings.length) console.warn('bpmn-js warnings:', warnings) const canvas = viewer.get('canvas') as any canvas.zoom('fit-viewport', 'auto') isImportedRef.current = true syncAutomationOverlays() }).catch((err: Error) => console.error('Error cargando BPMN:', err)) }, [xml, syncAutomationOverlays]) // ─── Re-sincronizar overlays cuando cambia la lista de automatizables ───── useEffect(() => { syncAutomationOverlays() }, [syncAutomationOverlays]) // ─── Click handler ───────────────────────────────────────────────────────── useEffect(() => { const viewer = viewerRef.current if (!viewer || !onElementClick) return const eventBus = viewer.get('eventBus') as any const handler = (event: any) => { const el = event.element if (!el) return const category = classifyElement(el.type) if (category === 'other') return onElementClick(el.id, el.businessObject?.name ?? el.id, category) } eventBus.on('element.click', handler) return () => eventBus.off('element.click', handler) }, [onElementClick]) // ─── Highlight del elemento seleccionado ────────────────────────────────── useEffect(() => { const viewer = viewerRef.current if (!viewer) return const canvas = viewer.get('canvas') as any const reg = viewer.get('elementRegistry') as any reg.forEach((el: any) => canvas.removeMarker(el.id, 'bpmn-selected')) if (selectedElementId) { try { canvas.addMarker(selectedElementId, 'bpmn-selected') } catch { /* ignorar */ } } }, [selectedElementId]) return
}