@@ -21,6 +21,21 @@ if (fs.existsSync(notifHtmlPath)) {
2121 console . warn ( 'email-notifications.html not found, links will be disabled' ) ;
2222}
2323
24+ // Alias map: user-facing events → notification page event they trigger downstream
25+ const NOTIF_ALIASES = {
26+ 'VIEW_AND_RESPOND_TO_DEFENCE' : 'claimant_response' ,
27+ 'APPLY_NOC_DECISION' : 'apply_noc_decision_defendant_lip' ,
28+ 'UPLOAD_TRANSLATED_DOCUMENT' : 'upload_translated_document_claimant_intention' ,
29+ } ;
30+
31+ function getNotifLink ( eventId ) {
32+ const lower = eventId . toLowerCase ( ) ;
33+ if ( notifPageEvents . has ( lower ) ) return lower ;
34+ const alias = NOTIF_ALIASES [ eventId ] ;
35+ if ( alias && notifPageEvents . has ( alias ) ) return alias ;
36+ return null ;
37+ }
38+
2439// Layout constants
2540const COL_W = 320 , PILL_W = 300 , PILL_H = 22 , PILL_GAP = 3 , PILL_R = 8 ;
2641const DEST_ROW_H = 14 ; // extra height per destination row
@@ -335,12 +350,12 @@ function renderColumn(stateId, x, y) {
335350
336351 // Pill background + event name (with suffix for duplicates)
337352 const displayName = displayNames . get ( ev . id ) || ev . name ;
338- const hasNotifLink = notifPageEvents . has ( ev . id . toLowerCase ( ) ) ;
353+ const notifTarget = getNotifLink ( ev . id ) ;
339354 svg += `<rect x="${ pillX } " y="${ py } " width="${ PILL_W } " height="${ PILL_H } " rx="${ PILL_R } " fill="${ bg } " stroke="#bbb" stroke-width="0.5"/>` ;
340355 svg += `<text x="${ pillX + 8 } " y="${ py + PILL_H / 2 + 3 } " font-size="${ FONT } " fill="#333" font-family="Arial">${ esc ( displayName ) } </text>` ;
341356
342357 // Notification link icon (envelope)
343- if ( hasNotifLink ) {
358+ if ( notifTarget ) {
344359 // Shift left if enabling condition icon is also present
345360 const hasEC = ! ! ev . enablingCondition ;
346361 const hasRightTag = info . type === 'stays' || info . type === 'unknown' ;
@@ -349,7 +364,7 @@ function renderColumn(stateId, x, y) {
349364 const tagShift = hasRightTag ? tagWidth : 0 ;
350365 const envX = pillX + PILL_W - 16 - ecShift - tagShift ;
351366 const envY = py + ( PILL_H - 12 ) / 2 ;
352- svg += `<a href="${ NOTIF_PAGE } #${ ev . id . toLowerCase ( ) } " target="_blank">` ;
367+ svg += `<a href="${ NOTIF_PAGE } #${ notifTarget } " target="_blank">` ;
353368 svg += `<g cursor="pointer">` ;
354369 svg += `<rect x="${ envX } " y="${ envY } " width="12" height="12" rx="3" fill="#e8f0fe" stroke="#1a56db" stroke-width="0.5"/>` ;
355370 svg += `<text x="${ envX + 6 } " y="${ envY + 9 } " text-anchor="middle" font-size="8" fill="#1a56db" font-family="Arial">\u2709</text>` ;
@@ -463,10 +478,59 @@ for (let i = 0; i < MAIN_FLOW.length - 1; i++) {
463478 spineSvg += `<line x1="${ x1 } " y1="${ y1 } " x2="${ x2 } " y2="${ y2 } " stroke="#4472c4" stroke-width="2" marker-end="url(#arrow)" opacity="0.35"/>` ;
464479}
465480
481+ // Global Events column (after the main flow)
482+ // Includes: all global user events + global camunda events that have notification links
483+ const globalColumnEventsAll = model . events . filter ( e =>
484+ globalIds . has ( e . id ) && ( e . sourceType === 'user' || getNotifLink ( e . id ) )
485+ ) ;
486+ const seenGlobalCol = new Set ( ) ;
487+ const globalUserEvents = globalColumnEventsAll . filter ( e => {
488+ if ( seenGlobalCol . has ( e . id ) ) return false ;
489+ seenGlobalCol . add ( e . id ) ;
490+ return true ;
491+ } ) . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
492+
493+ const guX = mainOffset + MAIN_FLOW . length * TOTAL_COL , guY = PAD + titleH ;
494+ const guPillX = guX + ( COL_W - PILL_W ) / 2 ;
495+ let guBodyH = PILL_GAP ;
496+ for ( let i = 0 ; i < globalUserEvents . length ; i ++ ) {
497+ guBodyH += PILL_H + PILL_GAP ;
498+ }
499+ guBodyH += PILL_GAP ;
500+ const guColH = HDR_H + guBodyH ;
501+ maxMainH = Math . max ( maxMainH , guColH ) ;
502+
503+ let guSvg = '' ;
504+ guSvg += `<rect x="${ guX } " y="${ guY } " width="${ COL_W } " height="${ guColH } " rx="4" fill="white" stroke="#ccc"/>` ;
505+ guSvg += `<rect x="${ guX } " y="${ guY } " width="${ COL_W } " height="${ HDR_H } " rx="4" fill="#5b4a8a"/>` ;
506+ guSvg += `<rect x="${ guX } " y="${ guY + HDR_H - 6 } " width="${ COL_W } " height="6" fill="#5b4a8a"/>` ;
507+ guSvg += `<text x="${ guX + COL_W / 2 } " y="${ guY + 17 } " text-anchor="middle" font-size="8.5" font-weight="bold" fill="${ HEADER_TEXT } " font-family="Arial">GLOBAL EVENTS</text>` ;
508+ guSvg += `<text x="${ guX + COL_W / 2 } " y="${ guY + 33 } " text-anchor="middle" font-size="7.5" fill="white" font-family="Arial">Available in all states (*\u2192*)</text>` ;
509+ const guDisplayNames = computeDisplayNames ( globalUserEvents ) ;
510+ let guPillY = guY + HDR_H + PILL_GAP ;
511+ for ( const ev of globalUserEvents ) {
512+ const bg = pillBg ( ev ) ;
513+ const guName = guDisplayNames . get ( ev . id ) || ev . name ;
514+ guSvg += `<rect x="${ guPillX } " y="${ guPillY } " width="${ PILL_W } " height="${ PILL_H } " rx="${ PILL_R } " fill="${ bg } " stroke="#bbb" stroke-width="0.5"/>` ;
515+ guSvg += `<text x="${ guPillX + 8 } " y="${ guPillY + PILL_H / 2 + 3 } " font-size="${ FONT } " fill="#333" font-family="Arial">${ esc ( guName ) } </text>` ;
516+ const guNotifTarget = getNotifLink ( ev . id ) ;
517+ if ( guNotifTarget ) {
518+ const envX = guPillX + PILL_W - 16 ;
519+ const envY = guPillY + ( PILL_H - 12 ) / 2 ;
520+ guSvg += `<a href="${ NOTIF_PAGE } #${ guNotifTarget } " target="_blank">` ;
521+ guSvg += `<g cursor="pointer">` ;
522+ guSvg += `<rect x="${ envX } " y="${ envY } " width="12" height="12" rx="3" fill="#e8f0fe" stroke="#1a56db" stroke-width="0.5"/>` ;
523+ guSvg += `<text x="${ envX + 6 } " y="${ envY + 9 } " text-anchor="middle" font-size="8" fill="#1a56db" font-family="Arial">\u2709</text>` ;
524+ guSvg += `<title>View notifications for ${ esc ( ev . id ) } </title>` ;
525+ guSvg += `</g></a>` ;
526+ }
527+ guPillY += PILL_H + PILL_GAP ;
528+ }
529+
466530// Exception row
467531const exY = PAD + titleH + maxMainH + 50 ;
468532let exSvg = '' , maxExH = 0 ;
469- const totalCols = MAIN_FLOW . length + 1 ; // +1 for Case Creation column
533+ const totalCols = MAIN_FLOW . length + 2 ; // +1 Case Creation, +1 Global User Events
470534const exOffset = mainOffset + Math . floor ( ( MAIN_FLOW . length - EXCEPTION_FLOW . length ) / 2 ) * TOTAL_COL ;
471535EXCEPTION_FLOW . forEach ( ( sid , i ) => {
472536 const x = exOffset + i * TOTAL_COL , y = exY ;
@@ -623,7 +687,7 @@ legSvg += `<text x="${legX+647}" y="${legY+62}" text-anchor="middle" font-size="
623687legSvg += `<rect x="${ legX + 620 } " y="${ legY + 26 } " width="55" height="16" rx="4" fill="#e8f0fe" stroke="#1a56db" stroke-width="0.5"/>` ;
624688legSvg += `<text x="${ legX + 647 } " y="${ legY + 38 } " text-anchor="middle" font-size="5.5" fill="#1a56db" font-family="Arial">\u2709 Triggers Notifs</text>` ;
625689
626- legSvg += `<text x="${ legX + 10 } " y="${ legY + 94 } " font-size="7" font-family="Arial" fill="#888">Generated ${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } | ${ model . summary . stateCount } states | ${ model . summary . eventCount } events | Global (*\u2192*) events not shown (${ globalIds . size } ) | ${ model . summary . resolvedDynamicEvents || 0 } dynamic transitions resolved from civil-service</text>` ;
690+ legSvg += `<text x="${ legX + 10 } " y="${ legY + 94 } " font-size="7" font-family="Arial" fill="#888">Generated ${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } | ${ model . summary . stateCount } states | ${ model . summary . eventCount } events | Global (*\u2192*) system events not shown (${ globalIds . size - globalUserEvents . length } ) | ${ model . summary . resolvedDynamicEvents || 0 } dynamic transitions resolved from civil-service</text>` ;
627691
628692const svgW = PAD * 2 + totalCols * TOTAL_COL ;
629693const svgH = legY + 115 ;
@@ -638,7 +702,7 @@ const svg = `<?xml version="1.0" encoding="UTF-8"?>
638702</defs>
639703<rect width="100%" height="100%" fill="white"/>
640704<text x="${ svgW / 2 } " y="${ PAD + 10 } " text-anchor="middle" font-size="14" font-weight="bold" font-family="Arial" fill="${ HEADER_BG } ">Civil CCD State & Event Model</text>
641- ${ ccSvg } ${ spineSvg } ${ mainSvg } ${ exSvg } ${ crossSvg } ${ legSvg }
705+ ${ ccSvg } ${ spineSvg } ${ mainSvg } ${ guSvg } ${ exSvg } ${ crossSvg } ${ legSvg }
642706</svg>` ;
643707
644708fs . writeFileSync ( 'build/state-event-model.svg' , svg ) ;
0 commit comments