@acorex-platform/module-workplacesafety-management-client 0.0.30 → 0.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/fesm2022/acorex-platform-module-workplacesafety-management-client-acorex-platform-module-workplacesafety-management-client-mzaaZ6T3.mjs +549 -0
  2. package/fesm2022/acorex-platform-module-workplacesafety-management-client-acorex-platform-module-workplacesafety-management-client-mzaaZ6T3.mjs.map +1 -0
  3. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-control-hierarchy-levels.mjs +509 -0
  4. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-control-hierarchy-levels.mjs.map +1 -0
  5. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-control-measure.mjs +297 -0
  6. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-control-measure.mjs.map +1 -0
  7. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard-ppe-rule.mjs +431 -0
  8. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard-ppe-rule.mjs.map +1 -0
  9. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard-register.mjs +416 -0
  10. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard-register.mjs.map +1 -0
  11. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard-risk.mjs +476 -0
  12. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard-risk.mjs.map +1 -0
  13. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard.mjs +482 -0
  14. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard.mjs.map +1 -0
  15. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-ppe-item.mjs +345 -0
  16. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-ppe-item.mjs.map +1 -0
  17. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-section.mjs +1458 -0
  18. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-section.mjs.map +1 -0
  19. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-type-section.mjs +1313 -0
  20. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-type-section.mjs.map +1 -0
  21. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-type.mjs +295 -0
  22. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-type.mjs.map +1 -0
  23. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-work-step.mjs +542 -0
  24. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-work-step.mjs.map +1 -0
  25. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan.mjs +470 -0
  26. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan.mjs.map +1 -0
  27. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-template.mjs +369 -0
  28. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-template.mjs.map +1 -0
  29. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-section-type.mjs +299 -0
  30. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-section-type.mjs.map +1 -0
  31. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-work-step-type.mjs +1358 -0
  32. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entities-work-step-type.mjs.map +1 -0
  33. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-CeTmhod5.mjs +170 -0
  34. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-CeTmhod5.mjs.map +1 -0
  35. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-control-hierarchy-levels.entity-Dae2kkZe.mjs +372 -0
  36. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-control-hierarchy-levels.entity-Dae2kkZe.mjs.map +1 -0
  37. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-control-measure.entity-CpNcN6Pf.mjs +160 -0
  38. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-control-measure.entity-CpNcN6Pf.mjs.map +1 -0
  39. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-hazard-ppe-rule.entity-BcsAnO9h.mjs +294 -0
  40. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-hazard-ppe-rule.entity-BcsAnO9h.mjs.map +1 -0
  41. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-hazard-register.entity-C9wGZ3hB.mjs +279 -0
  42. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-hazard-register.entity-C9wGZ3hB.mjs.map +1 -0
  43. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-hazard-risk.entity-AUr2GVLU.mjs +339 -0
  44. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-hazard-risk.entity-AUr2GVLU.mjs.map +1 -0
  45. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-hazard.entity-J6fveZKA.mjs +345 -0
  46. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-hazard.entity-J6fveZKA.mjs.map +1 -0
  47. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-ppe-item.entity-3fQPlBBf.mjs +208 -0
  48. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-ppe-item.entity-3fQPlBBf.mjs.map +1 -0
  49. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan-section-dependency-BRGcQo7H.mjs +971 -0
  50. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan-section-dependency-BRGcQo7H.mjs.map +1 -0
  51. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan-section.entity-BPx-3rYX.mjs +356 -0
  52. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan-section.entity-BPx-3rYX.mjs.map +1 -0
  53. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan-type-section.entity-_nG09ZpE.mjs +211 -0
  54. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan-type-section.entity-_nG09ZpE.mjs.map +1 -0
  55. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan-type.entity-CIH7W9R7.mjs +158 -0
  56. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan-type.entity-CIH7W9R7.mjs.map +1 -0
  57. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan-work-step.entity-DmFYsBhI.mjs +405 -0
  58. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan-work-step.entity-DmFYsBhI.mjs.map +1 -0
  59. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan.entity-CUDh_ZfQ.mjs +333 -0
  60. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-plan.entity-CUDh_ZfQ.mjs.map +1 -0
  61. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-template.entity-BxNUXvae.mjs +232 -0
  62. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-safety-template.entity-BxNUXvae.mjs.map +1 -0
  63. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-section-type.entity-73MUyaIk.mjs +162 -0
  64. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-section-type.entity-73MUyaIk.mjs.map +1 -0
  65. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-work-step-type.entity-Ujxum2GY.mjs +256 -0
  66. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog-work-step-type.entity-Ujxum2GY.mjs.map +1 -0
  67. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog.mjs +2 -0
  68. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog.mjs.map +1 -0
  69. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts.mjs +739 -806
  70. package/fesm2022/acorex-platform-module-workplacesafety-management-client-contracts.mjs.map +1 -1
  71. package/fesm2022/acorex-platform-module-workplacesafety-management-client-control-hierarchy-diagram-widget-view.component-CnTEwf_l.mjs +93 -0
  72. package/fesm2022/acorex-platform-module-workplacesafety-management-client-control-hierarchy-diagram-widget-view.component-CnTEwf_l.mjs.map +1 -0
  73. package/fesm2022/acorex-platform-module-workplacesafety-management-client-control-hierarchy-diagram.component-BI9VkzNw.mjs +117 -0
  74. package/fesm2022/acorex-platform-module-workplacesafety-management-client-control-hierarchy-diagram.component-BI9VkzNw.mjs.map +1 -0
  75. package/fesm2022/{acorex-platform-module-workplacesafety-management-client-control-hierarchy-diagram.service-BBhH7KP1.mjs → acorex-platform-module-workplacesafety-management-client-control-hierarchy-diagram.service-tMrUDnCb.mjs} +48 -1
  76. package/fesm2022/acorex-platform-module-workplacesafety-management-client-control-hierarchy-diagram.service-tMrUDnCb.mjs.map +1 -0
  77. package/fesm2022/{acorex-platform-module-workplacesafety-management-client-index-kF37zgd4.mjs → acorex-platform-module-workplacesafety-management-client-index-LxP1eZLY.mjs} +391 -92
  78. package/fesm2022/acorex-platform-module-workplacesafety-management-client-index-LxP1eZLY.mjs.map +1 -0
  79. package/fesm2022/acorex-platform-module-workplacesafety-management-client-local-entities.mjs +43 -0
  80. package/fesm2022/acorex-platform-module-workplacesafety-management-client-local-entities.mjs.map +1 -0
  81. package/fesm2022/acorex-platform-module-workplacesafety-management-client-ppe-item-viewer-widget-column.component-D2Mmow12.mjs +42 -0
  82. package/fesm2022/acorex-platform-module-workplacesafety-management-client-ppe-item-viewer-widget-column.component-D2Mmow12.mjs.map +1 -0
  83. package/fesm2022/acorex-platform-module-workplacesafety-management-client-ppe-item-viewer-widget-edit.component-CQtVhCgw.mjs +101 -0
  84. package/fesm2022/acorex-platform-module-workplacesafety-management-client-ppe-item-viewer-widget-edit.component-CQtVhCgw.mjs.map +1 -0
  85. package/fesm2022/acorex-platform-module-workplacesafety-management-client-ppe-item-viewer.service-YS8FHiRu.mjs +221 -0
  86. package/fesm2022/acorex-platform-module-workplacesafety-management-client-ppe-item-viewer.service-YS8FHiRu.mjs.map +1 -0
  87. package/fesm2022/acorex-platform-module-workplacesafety-management-client-ppe-item-viewer.util-2Kl3_3sg.mjs +61 -0
  88. package/fesm2022/acorex-platform-module-workplacesafety-management-client-ppe-item-viewer.util-2Kl3_3sg.mjs.map +1 -0
  89. package/fesm2022/{acorex-platform-module-workplacesafety-management-client-preview-control-hierarchy-diagram.command-C2X3VpEt.mjs → acorex-platform-module-workplacesafety-management-client-preview-control-hierarchy-diagram.command-Dd9xdo_y.mjs} +3 -7
  90. package/fesm2022/acorex-platform-module-workplacesafety-management-client-preview-control-hierarchy-diagram.command-Dd9xdo_y.mjs.map +1 -0
  91. package/fesm2022/acorex-platform-module-workplacesafety-management-client-safety-plan-preview-modal.component-6i0RkG3L.mjs +557 -0
  92. package/fesm2022/acorex-platform-module-workplacesafety-management-client-safety-plan-preview-modal.component-6i0RkG3L.mjs.map +1 -0
  93. package/fesm2022/{acorex-platform-module-workplacesafety-management-client-save-safety-plan-sections.command-BJsg3pHZ.mjs → acorex-platform-module-workplacesafety-management-client-save-safety-plan-sections.command-BlPzzCc2.mjs} +13 -25
  94. package/fesm2022/acorex-platform-module-workplacesafety-management-client-save-safety-plan-sections.command-BlPzzCc2.mjs.map +1 -0
  95. package/fesm2022/acorex-platform-module-workplacesafety-management-client.mjs +1 -460
  96. package/fesm2022/acorex-platform-module-workplacesafety-management-client.mjs.map +1 -1
  97. package/package.json +70 -2
  98. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-control-hierarchy-levels.d.ts +5 -0
  99. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-control-measure.d.ts +5 -0
  100. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard-ppe-rule.d.ts +5 -0
  101. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard-register.d.ts +5 -0
  102. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard-risk.d.ts +5 -0
  103. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-hazard.d.ts +5 -0
  104. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-ppe-item.d.ts +5 -0
  105. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-section.d.ts +9 -0
  106. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-type-section.d.ts +5 -0
  107. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-type.d.ts +5 -0
  108. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan-work-step.d.ts +5 -0
  109. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-plan.d.ts +5 -0
  110. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-safety-template.d.ts +5 -0
  111. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-section-type.d.ts +5 -0
  112. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entities-work-step-type.d.ts +5 -0
  113. package/types/acorex-platform-module-workplacesafety-management-client-contracts-entity-catalog.d.ts +10 -0
  114. package/types/acorex-platform-module-workplacesafety-management-client-contracts.d.ts +89 -140
  115. package/types/acorex-platform-module-workplacesafety-management-client-local-entities.d.ts +12 -0
  116. package/fesm2022/acorex-platform-module-workplacesafety-management-client-control-hierarchy-diagram-widget-view.component-C-qrrqOV.mjs +0 -203
  117. package/fesm2022/acorex-platform-module-workplacesafety-management-client-control-hierarchy-diagram-widget-view.component-C-qrrqOV.mjs.map +0 -1
  118. package/fesm2022/acorex-platform-module-workplacesafety-management-client-control-hierarchy-diagram.service-BBhH7KP1.mjs.map +0 -1
  119. package/fesm2022/acorex-platform-module-workplacesafety-management-client-hazard-risk-ppe-rule-status.provider-DtqzcWh0.mjs +0 -42
  120. package/fesm2022/acorex-platform-module-workplacesafety-management-client-hazard-risk-ppe-rule-status.provider-DtqzcWh0.mjs.map +0 -1
  121. package/fesm2022/acorex-platform-module-workplacesafety-management-client-index-kF37zgd4.mjs.map +0 -1
  122. package/fesm2022/acorex-platform-module-workplacesafety-management-client-preview-control-hierarchy-diagram.command-C2X3VpEt.mjs.map +0 -1
  123. package/fesm2022/acorex-platform-module-workplacesafety-management-client-save-safety-plan-sections.command-BJsg3pHZ.mjs.map +0 -1
@@ -1,3 +1,5 @@
1
+ import { createRandomUuid } from '@acorex-platform/framework-shared/core';
2
+
1
3
  //#region ---- GHS symbol options ----
2
4
  /** Asset base path for GHS pictograms (served from demo app assets). */
3
5
  const GHS_IMAGE_BASE = '/assets/workplacesafety-management/ghs';
@@ -113,13 +115,6 @@ const AXMWorkplaceSafetyManagementPermissionKeys = {
113
115
  Update: AXPSafetyRiskPermissionKeys.MaintainSafety,
114
116
  Delete: AXPSafetyRiskPermissionKeys.MaintainSafety,
115
117
  },
116
- HazardRiskPpeRule: {
117
- Management: AXPSafetyRiskPermissionKeys.MaintainSafety,
118
- View: AXPSafetyRiskPermissionKeys.MaintainSafety,
119
- Create: AXPSafetyRiskPermissionKeys.MaintainSafety,
120
- Update: AXPSafetyRiskPermissionKeys.MaintainSafety,
121
- Delete: AXPSafetyRiskPermissionKeys.MaintainSafety,
122
- },
123
118
  ControlHierarchyLevels: {
124
119
  Management: AXPSafetyRiskPermissionKeys.Setup,
125
120
  },
@@ -237,13 +232,6 @@ const RootConfig = {
237
232
  titlePlural: `@${config.i18n}:hazard-risks.entities.hazard-risk.plural`,
238
233
  icon: 'fa-light fa-shield-exclamation',
239
234
  },
240
- hazardRiskPpeRule: {
241
- name: 'HazardRiskPpeRule',
242
- source: `${config.module}.HazardRiskPpeRule`,
243
- title: `@${config.i18n}:hazard-risk-ppe-rules.entities.hazard-risk-ppe-rule.title`,
244
- titlePlural: `@${config.i18n}:hazard-risk-ppe-rules.entities.hazard-risk-ppe-rule.plural`,
245
- icon: 'fa-light fa-user-helmet-safety',
246
- },
247
235
  controlHierarchyLevels: {
248
236
  name: 'ControlHierarchyLevels',
249
237
  source: `${config.module}.ControlHierarchyLevels`,
@@ -414,7 +402,7 @@ const WORKPLACE_SAFETY_SECTION_TYPE_ENTITY_BY_CODE = {
414
402
  HazardRisk: `${moduleName}.${RootConfig.entities.hazardRisk.name}`,
415
403
  Ppe: `${moduleName}.${RootConfig.entities.ppeItem.name}`,
416
404
  PpeHazards: `${moduleName}.${RootConfig.entities.hazardPpeRule.name}`,
417
- HazardRiskPpe: `${moduleName}.${RootConfig.entities.hazardRiskPpeRule.name}`,
405
+ HazardRiskPpe: undefined,
418
406
  Control: `${moduleName}.${RootConfig.entities.controlMeasure.name}`,
419
407
  HierarchyLevel: `${moduleName}.${RootConfig.entities.controlHierarchyLevels.name}`,
420
408
  RiskMatrix: 'RiskManagement.RiskMatrixCell',
@@ -529,897 +517,845 @@ const WORKPLACE_SAFETY_SECTION_TYPE_ENTITY_EXPRESSION = buildSectionTypeEntityEx
529
517
  const WORKPLACE_SAFETY_SECTION_TYPE_ENTITY_EXPRESSION_SIBLING = buildSectionTypeEntityExpressionWithSnapshotFallback('sectionType.id', 'sectionType.itemEntityName');
530
518
  //#endregion
531
519
 
532
- //#region ---- Imports ----
533
- /** Declarative section-type dependencies resolved from prior sections in plan order. */
534
- const WORKPLACE_SAFETY_SECTION_TYPE_DEPENDENCIES = {
535
- HazardRisk: {
536
- kind: 'parentFilter',
537
- parentSectionTypeKey: 'Hazard',
538
- filterField: 'hazardId',
539
- },
540
- PpeHazards: {
541
- kind: 'parentFilter',
542
- parentSectionTypeKey: 'Hazard',
543
- filterField: 'hazardId',
544
- },
545
- HazardRiskPpe: {
546
- kind: 'parentFilter',
547
- parentSectionTypeKey: 'HazardRisk',
548
- filterField: 'hazardRiskId',
549
- },
550
- RiskMatrix: {
551
- kind: 'derivedFromParent',
552
- parentSectionTypeKey: 'HazardRisk',
553
- resolveField: 'riskMatrixId',
554
- filterField: 'id',
555
- },
556
- Control: {
557
- kind: 'derivedFromParent',
558
- parentSectionTypeKey: 'HazardRisk',
559
- resolveField: 'controlMeasureIds',
560
- filterField: 'id',
561
- },
562
- };
563
- /** Section types cleared when upstream hazard selections change. */
564
- const WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD = [
565
- 'HazardRisk',
566
- 'PpeHazards',
567
- 'HazardRiskPpe',
568
- 'RiskMatrix',
569
- 'Control',
570
- ];
571
- /** Section types cleared when upstream hazard-risk selections change. */
572
- const WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD_RISK = [
573
- 'HazardRiskPpe',
574
- 'RiskMatrix',
575
- 'Control',
576
- ];
577
- //#endregion
578
- //#region ---- Lookup helpers ----
579
- const PARENT_HAZARD_IDS_CONTEXT_KEY = 'parentHazardIds';
580
- const PARENT_HAZARD_RISK_IDS_CONTEXT_KEY = 'parentHazardRiskIds';
581
- /** Expose paths when a section type is selected (shared by entity repeater and builder dialog). */
582
- const WORKPLACE_SAFETY_SECTION_TYPE_LOOKUP_EXPOSE = [
583
- { source: 'id', target: 'sectionType.id' },
584
- { source: 'title', target: 'sectionType.title' },
585
- { source: 'code', target: 'sectionType.code' },
586
- { source: 'description', target: 'sectionType.description' },
587
- { source: 'isRepeatable', target: 'sectionType.isRepeatable' },
588
- ];
589
- /** Auto-fill section description when section type is selected. */
590
- const WORKPLACE_SAFETY_SECTION_TYPE_DESCRIPTION_SYNC_TRIGGERS = [
591
- {
592
- event: "{{ events.context('sectionTypeId') }}",
593
- action: "{{ const items = widget.output('selectedItems'); const selectedDescription = Array.isArray(items) ? items[0]?.description : undefined; const d = selectedDescription || context.eval('sectionType.description') || ''; context.set('sectionTypeDescription', d); const descriptionWidget = widget.find('sectionTypeDescription'); if (descriptionWidget) { descriptionWidget.setValue(d); } }}",
594
- },
595
- {
596
- event: "{{ events.context('sectionType.description') }}",
597
- action: "{{ const d = context.eval('sectionType.description') || ''; context.set('sectionTypeDescription', d); const descriptionWidget = widget.find('sectionTypeDescription'); if (descriptionWidget) { descriptionWidget.setValue(d); } }}",
598
- },
599
- ];
600
- /** Auto-fill section title when section type is selected. */
601
- const WORKPLACE_SAFETY_SECTION_TYPE_TITLE_SYNC_TRIGGERS = [
602
- {
603
- event: "{{ events.context('sectionTypeId') }}",
604
- action: "{{ const items = widget.output('selectedItems'); const selectedTitle = Array.isArray(items) ? items[0]?.title : undefined; const t = selectedTitle || context.eval('sectionType.title') || ''; if (t) { context.set('title', t); const titleWidget = widget.find('title'); if (titleWidget) { titleWidget.setValue(t); } } }}",
605
- },
606
- {
607
- event: "{{ events.context('sectionType.title') }}",
608
- action: "{{ const t = context.eval('sectionType.title') || ''; if (t) { context.set('title', t); const titleWidget = widget.find('title'); if (titleWidget) { titleWidget.setValue(t); } } }}",
609
- },
610
- ];
611
- /** Shared lookup widget options for section type pickers (builder dialog and entity forms). */
612
- const WORKPLACE_SAFETY_SECTION_TYPE_LOOKUP_WIDGET_OPTIONS = {
613
- look: 'select',
614
- allowSearch: true,
615
- textField: 'code',
616
- textTemplate: '{code} — {description}',
617
- valueField: 'id',
618
- columns: ['code', 'title', 'description'],
619
- expose: WORKPLACE_SAFETY_SECTION_TYPE_LOOKUP_EXPOSE,
620
- triggers: [
621
- ...WORKPLACE_SAFETY_SECTION_TYPE_TITLE_SYNC_TRIGGERS,
622
- ...WORKPLACE_SAFETY_SECTION_TYPE_DESCRIPTION_SYNC_TRIGGERS,
623
- ],
624
- };
625
- //#endregion
626
- //#region ---- Resolver API ----
627
- function getSectionTypeDependency(sectionTypeKey) {
628
- if (!sectionTypeKey) {
629
- return undefined;
520
+ /**
521
+ * Shared helpers for Safety Plan Section type identity (repeatable vs one-off).
522
+ * Leaf module — imports only other contract leaves (no client/server layers).
523
+ */
524
+ //#region ---- Helpers ----
525
+ function resolveRefId$1(raw) {
526
+ if (typeof raw === 'string' && raw.trim().length > 0) {
527
+ return raw.trim();
630
528
  }
631
- return WORKPLACE_SAFETY_SECTION_TYPE_DEPENDENCIES[sectionTypeKey];
632
- }
633
- function sectionTypeKeyFromSection(section) {
634
- const sectionTypeId = section.sectionTypeId || section.sectionType?.id;
635
- const key = resolveSectionTypeKey(sectionTypeId, section.sectionType);
636
- return key;
637
- }
638
- function collectSectionItemIds(section, sectionTypeKey) {
639
- if (!section?.items?.length || sectionTypeKeyFromSection(section) !== sectionTypeKey) {
640
- return [];
529
+ if (typeof raw === 'number' && Number.isFinite(raw)) {
530
+ return String(raw);
641
531
  }
642
- const ids = new Set();
643
- for (const item of section.items) {
644
- for (const id of item.itemIds ?? []) {
645
- if (typeof id === 'string' && id.length > 0) {
646
- ids.add(id);
647
- }
532
+ if (raw && typeof raw === 'object') {
533
+ const id = raw.id;
534
+ if (typeof id === 'string' && id.trim().length > 0) {
535
+ return id.trim();
648
536
  }
649
537
  }
650
- return [...ids];
651
- }
652
- /** Collects item IDs from sections up to `sectionIndex` whose type matches `parentSectionTypeKey`. */
653
- function resolveParentItemIds(sections, sectionIndex, parentSectionTypeKey) {
654
- const ids = new Set();
655
- const lastIndex = Math.min(sectionIndex, sections.length - 1);
656
- for (let index = 0; index <= lastIndex; index += 1) {
657
- collectSectionItemIds(sections[index], parentSectionTypeKey).forEach((id) => ids.add(id));
658
- }
659
- return [...ids];
660
- }
661
- /** Collects hazard IDs from earlier sections and Hazard rows in the current section. */
662
- function resolveParentHazardIds(sections, sectionIndex, parentSectionTypeKey = 'Hazard') {
663
- return resolveParentItemIds(sections, sectionIndex, parentSectionTypeKey);
664
- }
665
- /** Collects hazard-risk IDs from prior HazardRisk sections up to `sectionIndex`. */
666
- function resolveParentHazardRiskIds(sections, sectionIndex) {
667
- return resolveParentItemIds(sections, sectionIndex, 'HazardRisk');
668
- }
669
- function parentItemSignature(sections, sectionIndex, parentSectionTypeKey) {
670
- return resolveParentItemIds(sections, sectionIndex, parentSectionTypeKey).sort().join('|');
538
+ return '';
671
539
  }
672
- function shouldClearSectionOnParentChange(section, sectionIndex, changeIndex, parentTypeKey, downstreamTypes) {
673
- if (sectionIndex < changeIndex) {
540
+ /**
541
+ * True when this section type may appear more than once in a Safety Plan.
542
+ * Checks (in order): snapshot flag, explicit allow-list, built-in section type meta.
543
+ */
544
+ function isSafetyPlanSectionTypeRepeatable(sectionTypeId, options) {
545
+ const id = resolveRefId$1(sectionTypeId);
546
+ if (!id) {
674
547
  return false;
675
548
  }
676
- const sectionKey = sectionTypeKeyFromSection(section);
677
- if (!sectionKey || !downstreamTypes.includes(sectionKey)) {
678
- return false;
549
+ if (options?.sectionType?.isRepeatable === true) {
550
+ return true;
679
551
  }
680
- if (sectionIndex === changeIndex && sectionKey === parentTypeKey) {
681
- return false;
552
+ const allowList = new Set((options?.repeatableSectionTypeIds ?? []).map((entry) => resolveRefId$1(entry)).filter(Boolean));
553
+ if (allowList.has(id)) {
554
+ return true;
682
555
  }
683
- return section.items.some((item) => (item.itemIds?.length ?? 0) > 0);
556
+ return WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_BY_ID[id]?.isRepeatable === true;
684
557
  }
685
- function clearSectionItems(section) {
686
- return {
687
- ...section,
688
- items: section.items.map((item) => ({ ...item, itemIds: [], items: [] })),
689
- };
558
+ /** Built-in section type ids that are always treated as repeatable. */
559
+ function collectBuiltInRepeatableSectionTypeIds() {
560
+ return Object.entries(WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_BY_ID)
561
+ .filter(([, meta]) => meta.isRepeatable === true)
562
+ .map(([id]) => id);
690
563
  }
691
- /**
692
- * When upstream hazard or hazard-risk selections change, clears item rows in later dependent sections.
693
- */
694
- function clearDependentItemsOnParentChange(previous, next) {
695
- if (!next.sections.length) {
696
- return next;
564
+ //#endregion
565
+
566
+ //#region ---- Imports ----
567
+ //#endregion
568
+ //#region ---- Display helpers ----
569
+ /** Coerces a stored string or locale map into a single display string. */
570
+ function asSafetyPlanDisplayText(value) {
571
+ if (typeof value === 'string') {
572
+ return value.trim();
697
573
  }
698
- let changeIndex = next.sections.length;
699
- let hazardChanged = false;
700
- let hazardRiskChanged = false;
701
- for (let index = 0; index < next.sections.length; index += 1) {
702
- const prevHazardSig = parentItemSignature(previous.sections, index, 'Hazard');
703
- const nextHazardSig = parentItemSignature(next.sections, index, 'Hazard');
704
- if (prevHazardSig !== nextHazardSig) {
705
- changeIndex = Math.min(changeIndex, index);
706
- hazardChanged = true;
574
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
575
+ const rec = value;
576
+ for (const locale of ['en-US', 'en-AU', 'en', 'fa-IR']) {
577
+ const part = rec[locale];
578
+ if (typeof part === 'string' && part.trim()) {
579
+ return part.trim();
580
+ }
707
581
  }
708
- const prevRiskSig = parentItemSignature(previous.sections, index, 'HazardRisk');
709
- const nextRiskSig = parentItemSignature(next.sections, index, 'HazardRisk');
710
- if (prevRiskSig !== nextRiskSig) {
711
- changeIndex = Math.min(changeIndex, index);
712
- hazardRiskChanged = true;
582
+ for (const part of Object.values(rec)) {
583
+ if (typeof part === 'string' && part.trim()) {
584
+ return part.trim();
585
+ }
713
586
  }
714
587
  }
715
- if (changeIndex === next.sections.length) {
716
- return next;
717
- }
588
+ return '';
589
+ }
590
+ function buildSectionTypeRelationSnapshot(sectionTypeId, snapshot) {
591
+ const title = asSafetyPlanDisplayText(snapshot?.title);
592
+ const code = asSafetyPlanDisplayText(snapshot?.code);
593
+ const description = asSafetyPlanDisplayText(snapshot?.description);
718
594
  return {
719
- sections: next.sections.map((section, sectionIndex) => {
720
- const clearFromHazard = hazardChanged &&
721
- shouldClearSectionOnParentChange(section, sectionIndex, changeIndex, 'Hazard', WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD);
722
- const clearFromHazardRisk = hazardRiskChanged &&
723
- shouldClearSectionOnParentChange(section, sectionIndex, changeIndex, 'HazardRisk', WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD_RISK);
724
- if (!clearFromHazard && !clearFromHazardRisk) {
725
- return section;
726
- }
727
- return clearSectionItems(section);
728
- }),
595
+ id: sectionTypeId,
596
+ ...(title ? { title } : {}),
597
+ ...(code ? { code } : {}),
598
+ ...(description ? { description } : {}),
599
+ ...(snapshot?.isRepeatable === true ? { isRepeatable: true } : {}),
729
600
  };
730
601
  }
731
- function extractRiskMatrixIdsFromHazardRisks(records) {
732
- const ids = new Set();
733
- for (const record of records) {
734
- const id = record['riskMatrixId'];
735
- if (typeof id === 'string' && id.length > 0) {
736
- ids.add(id);
737
- }
602
+ function resolveSectionTypeTitle(sectionTypeId, sectionType) {
603
+ const title = asSafetyPlanDisplayText(sectionType?.title);
604
+ if (title) {
605
+ return title;
738
606
  }
739
- return [...ids];
607
+ const code = asSafetyPlanDisplayText(sectionType?.code);
608
+ if (code) {
609
+ return code;
610
+ }
611
+ const builtIn = WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_BY_ID[sectionTypeId]?.sectionTypeKey;
612
+ if (builtIn) {
613
+ return builtIn;
614
+ }
615
+ return sectionTypeId;
740
616
  }
741
- function extractControlMeasureIdsFromHazardRisks(records) {
742
- const ids = new Set();
743
- for (const record of records) {
744
- const rows = record['controlMeasureIds'];
745
- if (!Array.isArray(rows)) {
746
- continue;
747
- }
748
- for (const row of rows) {
749
- if (!row || typeof row !== 'object') {
750
- continue;
751
- }
752
- const entry = row;
753
- const controlMeasure = entry['controlMeasure'];
754
- const id = (typeof entry['controlMeasureId'] === 'string' && entry['controlMeasureId']) ||
755
- (typeof controlMeasure?.['id'] === 'string' && controlMeasure['id']) ||
756
- undefined;
757
- if (id) {
758
- ids.add(id);
759
- }
760
- }
617
+ /**
618
+ * Title persisted on SafetyPlanSection. Never stores a bare UUID — the UI translate
619
+ * pipe treats unknown keys as empty, which made seeded section headers blank.
620
+ */
621
+ function resolveSafetyPlanSectionPersistedTitle(sectionTitle, sectionTypeId, snapshot) {
622
+ const explicit = asSafetyPlanDisplayText(sectionTitle);
623
+ if (explicit && explicit !== sectionTypeId) {
624
+ return explicit;
761
625
  }
762
- return [...ids];
626
+ const fromType = resolveSectionTypeTitle(sectionTypeId, snapshot);
627
+ if (fromType && fromType !== sectionTypeId) {
628
+ return fromType;
629
+ }
630
+ return 'Section';
763
631
  }
764
- function buildInFilter(field, values) {
765
- const unique = [...new Set(values.filter((id) => id.length > 0))];
766
- if (!unique.length) {
767
- return null;
632
+ //#endregion
633
+ //#region ---- Ref helpers ----
634
+ /** True when a type-section row is marked as the plan-type default template. */
635
+ function isSafetyPlanTypeSectionDefault(row) {
636
+ const value = row?.isDefault;
637
+ return value === true || value === 1 || value === 'true';
638
+ }
639
+ /** Extracts a stable entity id from a raw lookup / context value. */
640
+ function resolveEntityRefId(raw) {
641
+ if (typeof raw === 'string' && raw.trim().length > 0) {
642
+ return raw.trim();
768
643
  }
769
- return {
770
- field,
771
- operator: { type: 'in' },
772
- value: unique,
773
- };
644
+ if (typeof raw === 'number' && Number.isFinite(raw)) {
645
+ return String(raw);
646
+ }
647
+ if (raw && typeof raw === 'object') {
648
+ const id = raw.id;
649
+ if (typeof id === 'string' && id.trim().length > 0) {
650
+ return id.trim();
651
+ }
652
+ }
653
+ return null;
774
654
  }
775
- /** Filter that matches no records when parent context exists but derived IDs are empty. */
776
- function buildEmptyResultFilter(field) {
655
+ function resolveSectionTypeId(typeRow) {
656
+ if (!typeRow) {
657
+ return '';
658
+ }
659
+ return (resolveEntityRefId(typeRow.sectionTypeId) ??
660
+ resolveEntityRefId(typeRow.sectionType) ??
661
+ resolveEntityRefId(typeRow.sectionType?.id) ??
662
+ '');
663
+ }
664
+ //#endregion
665
+ //#region ---- Build from type sections ----
666
+ let structureIdSeq = 0;
667
+ function nextStructureId(prefix) {
668
+ structureIdSeq += 1;
669
+ const rand = createRandomUuid();
670
+ return prefix ? `${prefix}-${rand}` : rand;
671
+ }
672
+ /**
673
+ * Builds an empty editable section structure from SafetyPlanTypeSection rows for one plan type.
674
+ *
675
+ * Uses `isDefault` rows when present; otherwise falls back to all active rows for that type
676
+ * (so a mis-flagged template still produces a structure). One plan section per `sectionTypeIds`
677
+ * entry, titled from the Section Type, ordered by type-section `sortOrder` then repeater order.
678
+ */
679
+ function buildSectionStructureFromTypeSections(typeSections, options) {
680
+ const createId = options?.createId ?? (() => nextStructureId(''));
681
+ const active = typeSections
682
+ .filter((row) => row && row.isArchived !== true)
683
+ .slice()
684
+ .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0) || String(a.title ?? '').localeCompare(String(b.title ?? '')));
685
+ const defaults = active.filter((row) => isSafetyPlanTypeSectionDefault(row));
686
+ const sourceRows = defaults.length > 0 ? defaults : active;
687
+ const sections = [];
688
+ const seenSourceTypePairs = new Set();
689
+ let sortOrder = 1;
690
+ for (const typeSection of sourceRows) {
691
+ const typeRows = Array.isArray(typeSection.sectionTypeIds) ? typeSection.sectionTypeIds : [];
692
+ if (!typeRows.length) {
693
+ continue;
694
+ }
695
+ for (const typeRow of typeRows) {
696
+ const sectionTypeId = resolveSectionTypeId(typeRow);
697
+ if (!sectionTypeId) {
698
+ continue;
699
+ }
700
+ // One plan section per (type-section source, section type) — prevents
701
+ // accidental double expansion when the same type is listed twice.
702
+ const sourceId = typeof typeSection.id === 'string' ? typeSection.id : '';
703
+ const pairKey = sourceId ? `${sourceId}|${sectionTypeId}` : sectionTypeId;
704
+ if (seenSourceTypePairs.has(pairKey)) {
705
+ continue;
706
+ }
707
+ seenSourceTypePairs.add(pairKey);
708
+ const sectionType = buildSectionTypeRelationSnapshot(sectionTypeId, typeRow.sectionType);
709
+ const sectionId = createId();
710
+ sections.push({
711
+ id: sectionId,
712
+ sectionTypeId,
713
+ sectionType,
714
+ title: resolveSafetyPlanSectionPersistedTitle(undefined, sectionTypeId, sectionType),
715
+ description: asSafetyPlanDisplayText(sectionType.description),
716
+ sourceSafetyPlanTypeSectionId: typeSection.id,
717
+ sortOrder: sortOrder++,
718
+ required: isSafetyPlanTypeSectionDefault(typeSection) || defaults.length === 0,
719
+ items: [
720
+ {
721
+ id: `${sectionId}__item_1`,
722
+ itemIds: [],
723
+ items: [],
724
+ },
725
+ ],
726
+ });
727
+ }
728
+ }
729
+ return { sections };
730
+ }
731
+ /**
732
+ * Removes accidental ghost rows from double materialization of the same type-section
733
+ * source. Distinct sections of the same type are kept. Reassigns contiguous 1-based
734
+ * `sortOrder` to preserve display order.
735
+ */
736
+ function dedupeSafetyPlanSectionStructure(structure, _options) {
737
+ const sections = Array.isArray(structure?.sections)
738
+ ? structure.sections
739
+ : [];
740
+ const seenSourceTypePairs = new Set();
741
+ const deduped = [];
742
+ for (const section of sections) {
743
+ const sectionTypeId = resolveEntityRefId(section.sectionTypeId) ?? String(section.sectionTypeId ?? '').trim();
744
+ if (!sectionTypeId) {
745
+ continue;
746
+ }
747
+ const sourceId = String(section.sourceSafetyPlanTypeSectionId ?? '').trim();
748
+ const sourcePairKey = sourceId ? `${sourceId}|${sectionTypeId}` : '';
749
+ // Same type-section source + section type must never appear twice (double materialization).
750
+ if (sourcePairKey && seenSourceTypePairs.has(sourcePairKey)) {
751
+ continue;
752
+ }
753
+ if (sourcePairKey) {
754
+ seenSourceTypePairs.add(sourcePairKey);
755
+ }
756
+ deduped.push(section);
757
+ }
777
758
  return {
778
- field,
779
- operator: { type: 'in' },
780
- value: ['00000000-0000-0000-0000-000000000000'],
759
+ ...structure,
760
+ sections: deduped.map((section, index) => ({
761
+ ...section,
762
+ sortOrder: index + 1,
763
+ })),
781
764
  };
782
765
  }
783
- function resolveSafetyPlanSectionItemFilter(sectionTypeId, parentHazardIds, parentHazardRiskIds = []) {
784
- const sectionTypeKey = resolveSectionTypeKey(sectionTypeId);
785
- const dependency = getSectionTypeDependency(sectionTypeKey);
786
- if (!dependency || dependency.kind !== 'parentFilter') {
787
- return null;
766
+ function collectAllowedSectionTypeIds(typeSections) {
767
+ const ids = new Set();
768
+ for (const typeSection of typeSections) {
769
+ if (typeSection.isArchived === true) {
770
+ continue;
771
+ }
772
+ for (const row of typeSection.sectionTypeIds ?? []) {
773
+ const id = resolveSectionTypeId(row);
774
+ if (id) {
775
+ ids.add(id);
776
+ }
777
+ }
788
778
  }
789
- const parentIds = dependency.parentSectionTypeKey === 'Hazard' ? parentHazardIds : parentHazardRiskIds;
790
- if (!parentIds.length) {
791
- return null;
779
+ return [...ids];
780
+ }
781
+ function collectRequiredSourceTypeSectionIds(typeSections) {
782
+ return typeSections
783
+ .filter((row) => row.isArchived !== true && isSafetyPlanTypeSectionDefault(row))
784
+ .map((row) => row.id)
785
+ .filter((id) => typeof id === 'string' && id.length > 0);
786
+ }
787
+ /**
788
+ * Validates that a plan-type section template does not list the same
789
+ * non-repeatable section type more than once (across rows and within a single row's repeater).
790
+ */
791
+ function validateSafetyPlanTypeSectionTemplate(typeSections, repeatableSectionTypeIds = []) {
792
+ const seen = new Map();
793
+ const issues = [];
794
+ for (const typeSection of typeSections) {
795
+ if (typeSection.isArchived === true) {
796
+ continue;
797
+ }
798
+ for (const row of typeSection.sectionTypeIds ?? []) {
799
+ const sectionTypeId = resolveSectionTypeId(row);
800
+ if (!sectionTypeId ||
801
+ isSafetyPlanSectionTypeRepeatable(sectionTypeId, {
802
+ repeatableSectionTypeIds,
803
+ sectionType: row.sectionType,
804
+ })) {
805
+ continue;
806
+ }
807
+ const sectionTypeTitle = resolveSafetyPlanSectionPersistedTitle(undefined, sectionTypeId, row.sectionType);
808
+ if (seen.has(sectionTypeId)) {
809
+ issues.push({
810
+ code: 'INVALID_TYPE_SECTION_TEMPLATE',
811
+ messageKey: '@workplacesafety-management:safety-plans.messages.section-structure.invalid-type-template-duplicate',
812
+ details: {
813
+ sectionTypeId,
814
+ sectionTypeTitle,
815
+ sourceSafetyPlanTypeSectionId: typeSection.id,
816
+ previousSourceSafetyPlanTypeSectionId: seen.get(sectionTypeId)?.sourceSafetyPlanTypeSectionId,
817
+ },
818
+ });
819
+ continue;
820
+ }
821
+ seen.set(sectionTypeId, {
822
+ title: sectionTypeTitle,
823
+ sourceSafetyPlanTypeSectionId: typeSection.id,
824
+ });
825
+ }
792
826
  }
793
- return buildInFilter(dependency.filterField, parentIds);
827
+ return issues;
794
828
  }
795
- async function resolveSafetyPlanSectionItemFilterAsync(sectionTypeId, sections, sectionIndex, loadHazardRisks) {
796
- const sectionTypeKey = resolveSectionTypeKey(sectionTypeId);
797
- const dependency = getSectionTypeDependency(sectionTypeKey);
798
- if (!dependency) {
799
- return null;
829
+ //#endregion
830
+ //#region ---- Validation ----
831
+ function validateSafetyPlanSectionStructure(structure, context) {
832
+ const sections = Array.isArray(structure?.sections) ? structure.sections : [];
833
+ const issues = [];
834
+ const presentSourceIds = new Set(sections
835
+ .map((s) => s.sourceSafetyPlanTypeSectionId)
836
+ .filter((id) => typeof id === 'string' && id.length > 0));
837
+ for (const requiredSourceId of context.requiredSourceTypeSectionIds) {
838
+ if (!presentSourceIds.has(requiredSourceId)) {
839
+ issues.push({
840
+ code: 'REQUIRED_SECTION_MISSING',
841
+ messageKey: '@workplacesafety-management:safety-plans.messages.section-structure.required-section-missing',
842
+ details: { sourceSafetyPlanTypeSectionId: requiredSourceId },
843
+ });
844
+ }
800
845
  }
801
- if (dependency.kind === 'parentFilter') {
802
- const parentIds = resolveParentItemIds(sections, sectionIndex, dependency.parentSectionTypeKey);
803
- if (!parentIds.length) {
804
- return null;
846
+ const allowed = new Set(context.allowedSectionTypeIds);
847
+ const sortOrders = [];
848
+ for (const section of sections) {
849
+ const sectionTypeId = resolveEntityRefId(section.sectionTypeId) ??
850
+ String(section.sectionTypeId ?? '').trim();
851
+ if (!sectionTypeId) {
852
+ continue;
853
+ }
854
+ const sectionType = section
855
+ .sectionType;
856
+ const sectionTypeTitle = resolveSafetyPlanSectionPersistedTitle(undefined, sectionTypeId, sectionType);
857
+ if (allowed.size > 0 && !allowed.has(sectionTypeId)) {
858
+ issues.push({
859
+ code: 'SECTION_TYPE_NOT_ALLOWED',
860
+ messageKey: '@workplacesafety-management:safety-plans.messages.section-structure.section-type-not-allowed',
861
+ details: { sectionTypeId, sectionTypeTitle, sectionId: section.id },
862
+ });
863
+ }
864
+ const sortOrder = section.sortOrder;
865
+ if (typeof sortOrder === 'number' && Number.isFinite(sortOrder)) {
866
+ sortOrders.push(sortOrder);
805
867
  }
806
- return buildInFilter(dependency.filterField, parentIds);
807
868
  }
808
- const parentIds = resolveParentItemIds(sections, sectionIndex, dependency.parentSectionTypeKey);
809
- if (!parentIds.length) {
810
- return null;
869
+ if (sortOrders.length !== sections.length) {
870
+ issues.push({
871
+ code: 'INVALID_SORT_ORDER',
872
+ messageKey: '@workplacesafety-management:safety-plans.messages.section-structure.invalid-sort-order',
873
+ });
811
874
  }
812
- const records = await loadHazardRisks(parentIds);
813
- const derivedIds = dependency.resolveField === 'riskMatrixId'
814
- ? extractRiskMatrixIdsFromHazardRisks(records)
815
- : extractControlMeasureIdsFromHazardRisks(records);
816
- if (!derivedIds.length) {
817
- return buildEmptyResultFilter(dependency.filterField);
875
+ else {
876
+ const unique = new Set(sortOrders);
877
+ if (unique.size !== sortOrders.length) {
878
+ issues.push({
879
+ code: 'INVALID_SORT_ORDER',
880
+ messageKey: '@workplacesafety-management:safety-plans.messages.section-structure.invalid-sort-order',
881
+ details: { reason: 'duplicate' },
882
+ });
883
+ }
818
884
  }
819
- return buildInFilter(dependency.filterField, derivedIds);
885
+ return issues;
820
886
  }
821
- const defaultItemIdsPlaceholderKey = '@workplacesafety-management:safety-plan-sections.entities.safety-plan-section.fields.item-ids.placeholder';
822
- /** Clears item selection when section type changes in item dialog context. */
823
- const WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_CLEAR_ON_SECTION_TYPE_TRIGGERS = [
824
- {
825
- event: "{{ events.context('sectionType.id') }}",
826
- action: "{{ widget.setValue([]); context.set('items', []); }}",
887
+ function toBuilderValueFromStructure(structure) {
888
+ return {
889
+ sections: structure.sections.map((section) => ({
890
+ id: section.id,
891
+ sectionTypeId: section.sectionTypeId,
892
+ sectionType: section.sectionType,
893
+ title: section.title,
894
+ description: section.description ?? '',
895
+ safetyPlanId: section.safetyPlanId,
896
+ code: section.code,
897
+ sourceSafetyPlanTypeSectionId: section.sourceSafetyPlanTypeSectionId,
898
+ sortOrder: section.sortOrder,
899
+ required: section.required,
900
+ items: section.items.map((item) => ({
901
+ id: item.id,
902
+ itemIds: [...(item.itemIds ?? [])],
903
+ items: item.items ? [...item.items] : [],
904
+ })),
905
+ })),
906
+ };
907
+ }
908
+ //#endregion
909
+
910
+ //#region ---- Imports ----
911
+ /** Declarative section-type dependencies resolved from prior sections in plan order. */
912
+ const WORKPLACE_SAFETY_SECTION_TYPE_DEPENDENCIES = {
913
+ HazardRisk: {
914
+ kind: 'parentFilter',
915
+ parentSectionTypeKey: 'Hazard',
916
+ filterField: 'hazardId',
917
+ },
918
+ PpeHazards: {
919
+ kind: 'parentFilter',
920
+ parentSectionTypeKey: 'Hazard',
921
+ filterField: 'hazardId',
922
+ },
923
+ HazardRiskPpe: {
924
+ kind: 'parentFilter',
925
+ parentSectionTypeKey: 'HazardRisk',
926
+ filterField: 'hazardRiskId',
827
927
  },
928
+ RiskMatrix: {
929
+ kind: 'derivedFromParent',
930
+ parentSectionTypeKey: 'HazardRisk',
931
+ resolveField: 'riskMatrixId',
932
+ filterField: 'id',
933
+ },
934
+ };
935
+ /** Section types cleared when upstream hazard selections change. */
936
+ const WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD = [
937
+ 'HazardRisk',
938
+ 'PpeHazards',
939
+ 'HazardRiskPpe',
940
+ 'RiskMatrix',
941
+ ];
942
+ /** Section types cleared when upstream hazard-risk selections change. */
943
+ const WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD_RISK = [
944
+ 'HazardRiskPpe',
945
+ 'RiskMatrix',
946
+ ];
947
+ //#endregion
948
+ //#region ---- Lookup helpers ----
949
+ const PARENT_HAZARD_IDS_CONTEXT_KEY = 'parentHazardIds';
950
+ const PARENT_HAZARD_RISK_IDS_CONTEXT_KEY = 'parentHazardRiskIds';
951
+ /** Expose paths when a section type is selected (shared by entity repeater and builder dialog). */
952
+ const WORKPLACE_SAFETY_SECTION_TYPE_LOOKUP_EXPOSE = [
953
+ { source: 'id', target: 'sectionType.id' },
954
+ { source: 'title', target: 'sectionType.title' },
955
+ { source: 'code', target: 'sectionType.code' },
956
+ { source: 'description', target: 'sectionType.description' },
957
+ { source: 'isRepeatable', target: 'sectionType.isRepeatable' },
958
+ ];
959
+ /** Auto-fill section description when section type is selected. */
960
+ const WORKPLACE_SAFETY_SECTION_TYPE_DESCRIPTION_SYNC_TRIGGERS = [
828
961
  {
829
962
  event: "{{ events.context('sectionTypeId') }}",
830
- action: "{{ widget.setValue([]); context.set('items', []); }}",
963
+ action: "{{ const items = widget.output('selectedItems'); const selectedDescription = Array.isArray(items) ? items[0]?.description : undefined; const d = selectedDescription || context.eval('sectionType.description') || ''; context.set('sectionTypeDescription', d); const descriptionWidget = widget.find('sectionTypeDescription'); if (descriptionWidget) { descriptionWidget.setValue(d); } }}",
831
964
  },
832
- ];
833
- /**
834
- * Reloads lookup data after section type is resolved (`sectionType.id`, `itemEntityName`).
835
- */
836
- const WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_REFRESH_ON_SECTION_TYPE_EXPOSE_TRIGGERS = [
837
965
  {
838
- event: "{{ events.context('sectionType.id') }}",
839
- action: '{{ widget.refresh(); }}',
966
+ event: "{{ events.context('sectionType.description') }}",
967
+ action: "{{ const d = context.eval('sectionType.description') || ''; context.set('sectionTypeDescription', d); const descriptionWidget = widget.find('sectionTypeDescription'); if (descriptionWidget) { descriptionWidget.setValue(d); } }}",
840
968
  },
969
+ ];
970
+ /** Auto-fill section title when section type is selected. */
971
+ const WORKPLACE_SAFETY_SECTION_TYPE_TITLE_SYNC_TRIGGERS = [
841
972
  {
842
- event: "{{ events.context('sectionType.itemEntityName') }}",
843
- action: '{{ widget.refresh(); }}',
973
+ event: "{{ events.context('sectionTypeId') }}",
974
+ action: "{{ const items = widget.output('selectedItems'); const selectedTitle = Array.isArray(items) ? items[0]?.title : undefined; const t = selectedTitle || context.eval('sectionType.title') || ''; if (t) { context.set('title', t); const titleWidget = widget.find('title'); if (titleWidget) { titleWidget.setValue(t); } } }}",
844
975
  },
845
976
  {
846
- event: "{{ events.context('sectionTypeId') }}",
847
- action: '{{ widget.refresh(); }}',
977
+ event: "{{ events.context('sectionType.title') }}",
978
+ action: "{{ const t = context.eval('sectionType.title') || ''; if (t) { context.set('title', t); const titleWidget = widget.find('title'); if (titleWidget) { titleWidget.setValue(t); } } }}",
848
979
  },
849
980
  ];
850
- const WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_SECTION_TYPE_TRIGGERS = [
851
- ...WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_CLEAR_ON_SECTION_TYPE_TRIGGERS,
852
- ...WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_REFRESH_ON_SECTION_TYPE_EXPOSE_TRIGGERS,
853
- ];
854
- function isPpeRuleSectionType(sectionTypeId) {
855
- return (sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.ppeHazards ||
856
- sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.hazardRiskPpe);
857
- }
858
- function buildSectionTypeItemTextFieldExpression(sectionTypeIdPath) {
859
- const ppeHazardsId = WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.ppeHazards;
860
- const hazardRiskPpeId = WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.hazardRiskPpe;
861
- return `{{ [ '${ppeHazardsId}', '${hazardRiskPpeId}' ].includes(context.eval('${sectionTypeIdPath}')) ? 'ppeItem.title' : 'title' }}`;
862
- }
863
- function buildSectionTypeItemColumnsExpression(sectionTypeIdPath) {
864
- const ppeHazardsId = WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.ppeHazards;
865
- const hazardRiskPpeId = WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.hazardRiskPpe;
866
- return `{{ [ '${ppeHazardsId}', '${hazardRiskPpeId}' ].includes(context.eval('${sectionTypeIdPath}')) ? ['code', 'ppeItem.title', 'requirementType'] : ['code', 'title'] }}`;
867
- }
868
- function buildSectionTypeDependentParentFilter(sectionTypeIdPath, parentIdsContextKey, parentSectionTypeKey) {
869
- const dependentSectionTypeIds = Object.entries(WORKPLACE_SAFETY_SECTION_TYPE_DEPENDENCIES)
870
- .filter(([, config]) => config.kind === 'parentFilter' && config.parentSectionTypeKey === parentSectionTypeKey)
871
- .map(([key]) => {
872
- const id = Object.entries(WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_BY_ID).find(([, meta]) => meta.sectionTypeKey === key)?.[0];
873
- return id ? `'${id}'` : undefined;
874
- })
875
- .filter((id) => Boolean(id))
876
- .join(',');
877
- return {
878
- field: parentSectionTypeKey === 'Hazard' ? 'hazardId' : 'hazardRiskId',
879
- operator: { type: 'in' },
880
- value: `{{ [${dependentSectionTypeIds}].includes(context.eval('${sectionTypeIdPath}')) ? ((context.eval('${parentIdsContextKey}') || []).length ? context.eval('${parentIdsContextKey}') : undefined) : undefined }}`,
881
- };
882
- }
883
- function buildSectionTypeDependentHazardFilter(sectionTypeIdPath, parentHazardIdsContextKey) {
884
- return buildSectionTypeDependentParentFilter(sectionTypeIdPath, parentHazardIdsContextKey, 'Hazard');
885
- }
886
- const sectionItemLookupExpose = [
887
- { source: 'id', target: 'items.{id}' },
888
- { source: 'title', target: 'items.{title}' },
889
- { source: 'code', target: 'items.{code}' },
890
- { source: 'ppeItem.id', target: 'items.{id}' },
891
- { source: 'ppeItem.title', target: 'items.{title}' },
892
- { source: 'ppeItem.code', target: 'items.{code}' },
893
- ];
894
- /** Shared item lookup options for builder item dialog (`sectionType.*` in dialog context). */
895
- function buildSafetyPlanSectionItemLookupOptions(entityExpression, options) {
896
- const sectionTypeIdPath = options?.sectionTypeIdPath ?? 'sectionType.id';
897
- const lookup = {
898
- entity: entityExpression,
899
- look: 'lookup',
900
- multiple: true,
901
- allowSearch: true,
902
- textField: buildSectionTypeItemTextFieldExpression(sectionTypeIdPath),
903
- columns: buildSectionTypeItemColumnsExpression(sectionTypeIdPath),
904
- placeholder: options?.defaultPlaceholderKey ?? defaultItemIdsPlaceholderKey,
905
- expose: sectionItemLookupExpose,
906
- triggers: WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_SECTION_TYPE_TRIGGERS,
907
- };
908
- const filters = [];
909
- if (options?.parentHazardIdsContextKey) {
910
- filters.push(buildSectionTypeDependentHazardFilter(sectionTypeIdPath, options.parentHazardIdsContextKey));
911
- }
912
- if (options?.parentHazardRiskIdsContextKey) {
913
- filters.push(buildSectionTypeDependentParentFilter(sectionTypeIdPath, options.parentHazardRiskIdsContextKey, 'HazardRisk'));
914
- }
915
- if (filters.length === 1) {
916
- lookup['filter'] = filters[0];
917
- }
918
- else if (filters.length > 1) {
919
- lookup['filter'] = { logic: 'and', filters };
981
+ /** Shared lookup widget options for section type pickers (builder dialog and entity forms). */
982
+ const WORKPLACE_SAFETY_SECTION_TYPE_LOOKUP_WIDGET_OPTIONS = {
983
+ look: 'select',
984
+ allowSearch: true,
985
+ textField: 'code',
986
+ textTemplate: '{code} — {description}',
987
+ valueField: 'id',
988
+ columns: ['code', 'title', 'description'],
989
+ expose: WORKPLACE_SAFETY_SECTION_TYPE_LOOKUP_EXPOSE,
990
+ triggers: [
991
+ ...WORKPLACE_SAFETY_SECTION_TYPE_TITLE_SYNC_TRIGGERS,
992
+ ...WORKPLACE_SAFETY_SECTION_TYPE_DESCRIPTION_SYNC_TRIGGERS,
993
+ ],
994
+ };
995
+ //#endregion
996
+ //#region ---- Resolver API ----
997
+ function getSectionTypeDependency(sectionTypeKey) {
998
+ if (!sectionTypeKey) {
999
+ return undefined;
920
1000
  }
921
- return lookup;
1001
+ return WORKPLACE_SAFETY_SECTION_TYPE_DEPENDENCIES[sectionTypeKey];
922
1002
  }
923
- /** Lookup options with entity data-selector popup UI (`look: 'lookup'`). */
924
- function buildSafetyPlanSectionItemSelectorOptions(entityExpression, options) {
925
- return {
926
- ...buildSafetyPlanSectionItemLookupOptions(entityExpression, options),
927
- look: 'lookup',
928
- };
1003
+ function sectionTypeKeyFromSection(section) {
1004
+ const sectionTypeId = section.sectionTypeId || section.sectionType?.id;
1005
+ const key = resolveSectionTypeKey(sectionTypeId, section.sectionType);
1006
+ return key;
929
1007
  }
930
- function resolveSafetyPlanSectionItemColumns(sectionTypeId) {
931
- if (sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.ppeHazards) {
932
- return ['code', 'ppeItem.title', 'requirementType'];
933
- }
934
- if (sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.hazardRiskPpe) {
935
- return ['code', 'hazardRisk.title', 'ppeItem.title', 'requirementType'];
936
- }
937
- if (sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.riskMatrix) {
938
- return ['code', 'title'];
1008
+ function collectSectionItemIds(section, sectionTypeKey) {
1009
+ if (!section?.items?.length || sectionTypeKeyFromSection(section) !== sectionTypeKey) {
1010
+ return [];
939
1011
  }
940
- return ['code', 'title'];
941
- }
942
- function resolveSafetyPlanSectionItemEntityKey(sectionTypeId, sectionType) {
943
- return resolveSectionItemEntityName(sectionTypeId, sectionType);
944
- }
945
- function mapSafetyPlanSectionSelectedItems(sectionTypeId, selectedItems) {
946
- const isPpeHazards = sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.ppeHazards;
947
- const isHazardRiskPpe = sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.hazardRiskPpe;
948
- return selectedItems
949
- .map((item) => {
950
- if (isPpeHazards || isHazardRiskPpe) {
951
- const ppeItem = item['ppeItem'];
952
- const id = String(ppeItem?.['id'] ?? item['id'] ?? '');
953
- return {
954
- id,
955
- title: typeof ppeItem?.['title'] === 'string' ? ppeItem['title'] : undefined,
956
- code: typeof ppeItem?.['code'] === 'string' ? ppeItem['code'] : undefined,
957
- };
1012
+ const ids = new Set();
1013
+ for (const item of section.items) {
1014
+ for (const id of item.itemIds ?? []) {
1015
+ if (typeof id === 'string' && id.length > 0) {
1016
+ ids.add(id);
1017
+ }
958
1018
  }
959
- return {
960
- id: String(item['id'] ?? ''),
961
- title: typeof item['title'] === 'string' ? item['title'] : undefined,
962
- code: typeof item['code'] === 'string' ? item['code'] : undefined,
963
- };
964
- })
965
- .filter((item) => item.id.length > 0);
1019
+ }
1020
+ return [...ids];
966
1021
  }
967
- function dedupeSafetyPlanSectionItemSelection(item) {
968
- const seen = new Set();
969
- const itemIds = [];
970
- const items = [];
971
- for (let index = 0; index < (item.itemIds?.length ?? 0); index += 1) {
972
- const id = item.itemIds?.[index];
973
- if (typeof id !== 'string' || !id || seen.has(id)) {
974
- continue;
975
- }
976
- seen.add(id);
977
- itemIds.push(id);
978
- const snapshot = item.items?.[index];
979
- items.push(snapshot?.id === id ? snapshot : { id });
1022
+ /** Collects item IDs from sections up to `sectionIndex` whose type matches `parentSectionTypeKey`. */
1023
+ function resolveParentItemIds(sections, sectionIndex, parentSectionTypeKey) {
1024
+ const ids = new Set();
1025
+ const lastIndex = Math.min(sectionIndex, sections.length - 1);
1026
+ for (let index = 0; index <= lastIndex; index += 1) {
1027
+ collectSectionItemIds(sections[index], parentSectionTypeKey).forEach((id) => ids.add(id));
980
1028
  }
981
- return { ...item, itemIds, items };
1029
+ return [...ids];
982
1030
  }
983
- function isPpeRuleSectionTypeId(sectionTypeId) {
984
- return isPpeRuleSectionType(sectionTypeId);
1031
+ /** Collects hazard IDs from earlier sections and Hazard rows in the current section. */
1032
+ function resolveParentHazardIds(sections, sectionIndex, parentSectionTypeKey = 'Hazard') {
1033
+ return resolveParentItemIds(sections, sectionIndex, parentSectionTypeKey);
985
1034
  }
986
- //#endregion
987
-
988
- /** Component key for the safety plan Sections page tab. Register via AXP_PAGE_COMPONENT_PROVIDER. */
989
- const SAFETY_PLAN_SECTIONS_PAGE_COMPONENT_KEY = 'safety-plan-sections-page';
990
-
991
- //#region ---- Safety plan section builder value types ----
992
- //#endregion
993
-
994
- /**
995
- * Shared helpers for Safety Plan Section uniqueness (section type per plan).
996
- * Leaf module — imports only other contract leaves (no client/server layers).
997
- */
998
- //#region ---- Constants ----
999
- const SAFETY_PLAN_SECTION_DUPLICATE_SECTION_TYPE_MESSAGE_KEY = '@workplacesafety-management:safety-plan-sections.messages.duplicate-section-type';
1000
- /** Stable API / middleware error code for duplicate section type in a plan. */
1001
- const SAFETY_PLAN_SECTION_DUPLICATE_SECTION_TYPE_CODE = 'SAFETY_PLAN_SECTION_DUPLICATE_SECTION_TYPE';
1002
- //#endregion
1003
- //#region ---- Helpers ----
1004
- function resolveRefId$1(raw) {
1005
- if (typeof raw === 'string' && raw.trim().length > 0) {
1006
- return raw.trim();
1007
- }
1008
- if (typeof raw === 'number' && Number.isFinite(raw)) {
1009
- return String(raw);
1010
- }
1011
- if (raw && typeof raw === 'object') {
1012
- const id = raw.id;
1013
- if (typeof id === 'string' && id.trim().length > 0) {
1014
- return id.trim();
1015
- }
1016
- }
1017
- return '';
1035
+ /** Collects hazard-risk IDs from prior HazardRisk sections up to `sectionIndex`. */
1036
+ function resolveParentHazardRiskIds(sections, sectionIndex) {
1037
+ return resolveParentItemIds(sections, sectionIndex, 'HazardRisk');
1018
1038
  }
1019
- /** Resolves the section type id from a plan section identity or nested snapshot. */
1020
- function resolveSafetyPlanSectionTypeId(section) {
1021
- return resolveRefId$1(section.sectionTypeId) || resolveRefId$1(section.sectionType);
1039
+ function parentItemSignature(sections, sectionIndex, parentSectionTypeKey) {
1040
+ return resolveParentItemIds(sections, sectionIndex, parentSectionTypeKey).sort().join('|');
1022
1041
  }
1023
- /**
1024
- * True when this section type may appear more than once in a Safety Plan.
1025
- * Checks (in order): snapshot flag, explicit allow-list, built-in section type meta.
1026
- */
1027
- function isSafetyPlanSectionTypeRepeatable(sectionTypeId, options) {
1028
- const id = resolveRefId$1(sectionTypeId);
1029
- if (!id) {
1042
+ function shouldClearSectionOnParentChange(section, sectionIndex, changeIndex, parentTypeKey, downstreamTypes) {
1043
+ if (sectionIndex < changeIndex) {
1030
1044
  return false;
1031
1045
  }
1032
- if (options?.sectionType?.isRepeatable === true) {
1033
- return true;
1046
+ const sectionKey = sectionTypeKeyFromSection(section);
1047
+ if (!sectionKey || !downstreamTypes.includes(sectionKey)) {
1048
+ return false;
1034
1049
  }
1035
- const allowList = new Set((options?.repeatableSectionTypeIds ?? []).map((entry) => resolveRefId$1(entry)).filter(Boolean));
1036
- if (allowList.has(id)) {
1037
- return true;
1050
+ if (sectionIndex === changeIndex && sectionKey === parentTypeKey) {
1051
+ return false;
1038
1052
  }
1039
- return WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_BY_ID[id]?.isRepeatable === true;
1040
- }
1041
- /** Built-in section type ids that are always treated as repeatable. */
1042
- function collectBuiltInRepeatableSectionTypeIds() {
1043
- return Object.entries(WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_BY_ID)
1044
- .filter(([, meta]) => meta.isRepeatable === true)
1045
- .map(([id]) => id);
1053
+ return section.items.some((item) => (item.itemIds?.length ?? 0) > 0);
1054
+ }
1055
+ function clearSectionItems(section) {
1056
+ return {
1057
+ ...section,
1058
+ items: section.items.map((item) => ({ ...item, itemIds: [], items: [] })),
1059
+ };
1046
1060
  }
1047
- //#endregion
1048
- //#region ---- Validation ----
1049
1061
  /**
1050
- * Returns the first duplicate non-repeatable `sectionTypeId` in the list, or `null`
1051
- * when all non-repeatable types are unique. Empty / missing section type ids are ignored.
1052
- * Repeatable section types are allowed to appear more than once.
1062
+ * When upstream hazard or hazard-risk selections change, clears item rows in later dependent sections.
1053
1063
  */
1054
- function findDuplicateSafetyPlanSectionTypeId(sections, options) {
1055
- const excludeId = options?.excludeSectionId?.trim() || '';
1056
- const seen = new Set();
1057
- for (const section of sections) {
1058
- const id = resolveRefId$1(section.id);
1059
- if (excludeId && id === excludeId) {
1060
- continue;
1061
- }
1062
- const sectionTypeId = resolveSafetyPlanSectionTypeId(section);
1063
- if (!sectionTypeId ||
1064
- isSafetyPlanSectionTypeRepeatable(sectionTypeId, {
1065
- repeatableSectionTypeIds: options?.repeatableSectionTypeIds,
1066
- sectionType: section.sectionType,
1067
- })) {
1068
- continue;
1064
+ function clearDependentItemsOnParentChange(previous, next) {
1065
+ if (!next.sections.length) {
1066
+ return next;
1067
+ }
1068
+ let changeIndex = next.sections.length;
1069
+ let hazardChanged = false;
1070
+ let hazardRiskChanged = false;
1071
+ for (let index = 0; index < next.sections.length; index += 1) {
1072
+ const prevHazardSig = parentItemSignature(previous.sections, index, 'Hazard');
1073
+ const nextHazardSig = parentItemSignature(next.sections, index, 'Hazard');
1074
+ if (prevHazardSig !== nextHazardSig) {
1075
+ changeIndex = Math.min(changeIndex, index);
1076
+ hazardChanged = true;
1069
1077
  }
1070
- if (seen.has(sectionTypeId)) {
1071
- return sectionTypeId;
1078
+ const prevRiskSig = parentItemSignature(previous.sections, index, 'HazardRisk');
1079
+ const nextRiskSig = parentItemSignature(next.sections, index, 'HazardRisk');
1080
+ if (prevRiskSig !== nextRiskSig) {
1081
+ changeIndex = Math.min(changeIndex, index);
1082
+ hazardRiskChanged = true;
1072
1083
  }
1073
- seen.add(sectionTypeId);
1074
1084
  }
1075
- return null;
1076
- }
1077
- /**
1078
- * True when `sectionTypeId` already appears on another section in the collection
1079
- * and the type is not repeatable.
1080
- */
1081
- function safetyPlanSectionTypeAlreadyExists(sections, sectionTypeId, options) {
1082
- const target = resolveRefId$1(sectionTypeId);
1083
- if (!target) {
1084
- return false;
1085
- }
1086
- if (isSafetyPlanSectionTypeRepeatable(target, {
1087
- repeatableSectionTypeIds: options?.repeatableSectionTypeIds,
1088
- sectionType: options?.sectionType,
1089
- })) {
1090
- return false;
1085
+ if (changeIndex === next.sections.length) {
1086
+ return next;
1091
1087
  }
1092
- const excludeId = options?.excludeSectionId?.trim() || '';
1093
- return sections.some((section) => {
1094
- const id = resolveRefId$1(section.id);
1095
- if (excludeId && id === excludeId) {
1096
- return false;
1097
- }
1098
- return resolveSafetyPlanSectionTypeId(section) === target;
1099
- });
1088
+ return {
1089
+ sections: next.sections.map((section, sectionIndex) => {
1090
+ const clearFromHazard = hazardChanged &&
1091
+ shouldClearSectionOnParentChange(section, sectionIndex, changeIndex, 'Hazard', WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD);
1092
+ const clearFromHazardRisk = hazardRiskChanged &&
1093
+ shouldClearSectionOnParentChange(section, sectionIndex, changeIndex, 'HazardRisk', WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD_RISK);
1094
+ if (!clearFromHazard && !clearFromHazardRisk) {
1095
+ return section;
1096
+ }
1097
+ return clearSectionItems(section);
1098
+ }),
1099
+ };
1100
1100
  }
1101
- //#endregion
1102
-
1103
- //#region ---- Imports ----
1104
- function resolveSectionTypeTitle(sectionTypeId, sectionType) {
1105
- if (typeof sectionType?.title === 'string' && sectionType.title.trim()) {
1106
- return sectionType.title.trim();
1101
+ function extractRiskMatrixIdsFromHazardRisks(records) {
1102
+ const ids = new Set();
1103
+ for (const record of records) {
1104
+ const id = record['riskMatrixId'];
1105
+ if (typeof id === 'string' && id.length > 0) {
1106
+ ids.add(id);
1107
+ }
1107
1108
  }
1108
- if (typeof sectionType?.code === 'string' && sectionType.code.trim()) {
1109
- return sectionType.code.trim();
1109
+ return [...ids];
1110
+ }
1111
+ function buildInFilter(field, values) {
1112
+ const unique = [...new Set(values.filter((id) => id.length > 0))];
1113
+ if (!unique.length) {
1114
+ return null;
1110
1115
  }
1111
- return sectionTypeId;
1116
+ return {
1117
+ field,
1118
+ operator: { type: 'in' },
1119
+ value: unique,
1120
+ };
1112
1121
  }
1113
- /** True when a type-section row is marked as the plan-type default template. */
1114
- function isSafetyPlanTypeSectionDefault(row) {
1115
- const value = row?.isDefault;
1116
- return value === true || value === 1 || value === 'true';
1122
+ /** Filter that matches no records when parent context exists but derived IDs are empty. */
1123
+ function buildEmptyResultFilter(field) {
1124
+ return {
1125
+ field,
1126
+ operator: { type: 'in' },
1127
+ value: ['00000000-0000-0000-0000-000000000000'],
1128
+ };
1117
1129
  }
1118
- /** Extracts a stable entity id from a raw lookup / context value. */
1119
- function resolveEntityRefId(raw) {
1120
- if (typeof raw === 'string' && raw.trim().length > 0) {
1121
- return raw.trim();
1130
+ function resolveSafetyPlanSectionItemFilter(sectionTypeId, parentHazardIds, parentHazardRiskIds = []) {
1131
+ const sectionTypeKey = resolveSectionTypeKey(sectionTypeId);
1132
+ const dependency = getSectionTypeDependency(sectionTypeKey);
1133
+ if (!dependency || dependency.kind !== 'parentFilter') {
1134
+ return null;
1122
1135
  }
1123
- if (typeof raw === 'number' && Number.isFinite(raw)) {
1124
- return String(raw);
1136
+ const parentIds = dependency.parentSectionTypeKey === 'Hazard' ? parentHazardIds : parentHazardRiskIds;
1137
+ if (!parentIds.length) {
1138
+ return null;
1125
1139
  }
1126
- if (raw && typeof raw === 'object') {
1127
- const id = raw.id;
1128
- if (typeof id === 'string' && id.trim().length > 0) {
1129
- return id.trim();
1140
+ return buildInFilter(dependency.filterField, parentIds);
1141
+ }
1142
+ async function resolveSafetyPlanSectionItemFilterAsync(sectionTypeId, sections, sectionIndex, loadHazardRisks) {
1143
+ const sectionTypeKey = resolveSectionTypeKey(sectionTypeId);
1144
+ const dependency = getSectionTypeDependency(sectionTypeKey);
1145
+ if (!dependency) {
1146
+ return null;
1147
+ }
1148
+ if (dependency.kind === 'parentFilter') {
1149
+ const parentIds = resolveParentItemIds(sections, sectionIndex, dependency.parentSectionTypeKey);
1150
+ if (!parentIds.length) {
1151
+ return null;
1130
1152
  }
1153
+ return buildInFilter(dependency.filterField, parentIds);
1131
1154
  }
1132
- return null;
1133
- }
1134
- function resolveSectionTypeId(typeRow) {
1135
- if (!typeRow) {
1136
- return '';
1155
+ const parentIds = resolveParentItemIds(sections, sectionIndex, dependency.parentSectionTypeKey);
1156
+ if (!parentIds.length) {
1157
+ return null;
1137
1158
  }
1138
- return (resolveEntityRefId(typeRow.sectionTypeId) ??
1139
- resolveEntityRefId(typeRow.sectionType) ??
1140
- resolveEntityRefId(typeRow.sectionType?.id) ??
1141
- '');
1142
- }
1143
- //#endregion
1144
- //#region ---- Build from type sections ----
1145
- let structureIdSeq = 0;
1146
- function nextStructureId(prefix) {
1147
- structureIdSeq += 1;
1148
- const rand = typeof crypto !== 'undefined' && 'randomUUID' in crypto
1149
- ? crypto.randomUUID()
1150
- : `00000000-0000-4000-8000-${String(structureIdSeq).padStart(12, '0')}`;
1151
- return prefix ? `${prefix}-${rand}` : rand;
1159
+ const records = await loadHazardRisks(parentIds);
1160
+ const derivedIds = extractRiskMatrixIdsFromHazardRisks(records);
1161
+ if (!derivedIds.length) {
1162
+ return buildEmptyResultFilter(dependency.filterField);
1163
+ }
1164
+ return buildInFilter(dependency.filterField, derivedIds);
1152
1165
  }
1166
+ const defaultItemIdsPlaceholderKey = '@workplacesafety-management:safety-plan-sections.entities.safety-plan-section.fields.item-ids.placeholder';
1167
+ /** Clears item selection when section type changes in item dialog context. */
1168
+ const WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_CLEAR_ON_SECTION_TYPE_TRIGGERS = [
1169
+ {
1170
+ event: "{{ events.context('sectionType.id') }}",
1171
+ action: "{{ widget.setValue([]); context.set('items', []); }}",
1172
+ },
1173
+ {
1174
+ event: "{{ events.context('sectionTypeId') }}",
1175
+ action: "{{ widget.setValue([]); context.set('items', []); }}",
1176
+ },
1177
+ ];
1153
1178
  /**
1154
- * Builds an empty editable section structure from SafetyPlanTypeSection rows for one plan type.
1155
- *
1156
- * Uses `isDefault` rows when present; otherwise falls back to all active rows for that type
1157
- * (so a mis-flagged template still produces a structure). One plan section per `sectionTypeIds`
1158
- * entry, titled from the Section Type, ordered by type-section `sortOrder` then repeater order.
1179
+ * Reloads lookup data after section type is resolved (`sectionType.id`, `itemEntityName`).
1159
1180
  */
1160
- function buildSectionStructureFromTypeSections(typeSections, options) {
1161
- const createId = options?.createId ?? (() => nextStructureId(''));
1162
- const active = typeSections
1163
- .filter((row) => row && row.isArchived !== true)
1164
- .slice()
1165
- .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0) || String(a.title ?? '').localeCompare(String(b.title ?? '')));
1166
- const defaults = active.filter((row) => isSafetyPlanTypeSectionDefault(row));
1167
- const sourceRows = defaults.length > 0 ? defaults : active;
1168
- const sections = [];
1169
- const seenSourceTypePairs = new Set();
1170
- let sortOrder = 1;
1171
- for (const typeSection of sourceRows) {
1172
- const typeRows = Array.isArray(typeSection.sectionTypeIds) ? typeSection.sectionTypeIds : [];
1173
- if (!typeRows.length) {
1174
- continue;
1175
- }
1176
- for (const typeRow of typeRows) {
1177
- const sectionTypeId = resolveSectionTypeId(typeRow);
1178
- if (!sectionTypeId) {
1179
- continue;
1180
- }
1181
- // One plan section per (type-section source, section type) — prevents
1182
- // accidental double expansion when the same type is listed twice.
1183
- const sourceId = typeof typeSection.id === 'string' ? typeSection.id : '';
1184
- const pairKey = sourceId ? `${sourceId}|${sectionTypeId}` : sectionTypeId;
1185
- if (seenSourceTypePairs.has(pairKey)) {
1186
- continue;
1187
- }
1188
- seenSourceTypePairs.add(pairKey);
1189
- const sectionType = typeRow.sectionType
1190
- ? { id: sectionTypeId, title: typeRow.sectionType.title, code: typeRow.sectionType.code }
1191
- : { id: sectionTypeId };
1192
- const sectionId = createId();
1193
- sections.push({
1194
- id: sectionId,
1195
- sectionTypeId,
1196
- sectionType,
1197
- title: resolveSectionTypeTitle(sectionTypeId, sectionType),
1198
- description: '',
1199
- sourceSafetyPlanTypeSectionId: typeSection.id,
1200
- sortOrder: sortOrder++,
1201
- required: isSafetyPlanTypeSectionDefault(typeSection) || defaults.length === 0,
1202
- items: [
1203
- {
1204
- id: `${sectionId}__item_1`,
1205
- itemIds: [],
1206
- items: [],
1207
- },
1208
- ],
1209
- });
1210
- }
1211
- }
1212
- return { sections };
1181
+ const WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_REFRESH_ON_SECTION_TYPE_EXPOSE_TRIGGERS = [
1182
+ {
1183
+ event: "{{ events.context('sectionType.id') }}",
1184
+ action: '{{ widget.refresh(); }}',
1185
+ },
1186
+ {
1187
+ event: "{{ events.context('sectionType.itemEntityName') }}",
1188
+ action: '{{ widget.refresh(); }}',
1189
+ },
1190
+ {
1191
+ event: "{{ events.context('sectionTypeId') }}",
1192
+ action: '{{ widget.refresh(); }}',
1193
+ },
1194
+ ];
1195
+ const WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_SECTION_TYPE_TRIGGERS = [
1196
+ ...WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_CLEAR_ON_SECTION_TYPE_TRIGGERS,
1197
+ ...WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_REFRESH_ON_SECTION_TYPE_EXPOSE_TRIGGERS,
1198
+ ];
1199
+ function isPpeRuleSectionType(sectionTypeId) {
1200
+ return (sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.ppeHazards ||
1201
+ sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.hazardRiskPpe);
1202
+ }
1203
+ function buildSectionTypeItemTextFieldExpression(sectionTypeIdPath) {
1204
+ const ppeHazardsId = WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.ppeHazards;
1205
+ const hazardRiskPpeId = WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.hazardRiskPpe;
1206
+ return `{{ [ '${ppeHazardsId}', '${hazardRiskPpeId}' ].includes(context.eval('${sectionTypeIdPath}')) ? 'ppeItem.title' : 'title' }}`;
1207
+ }
1208
+ function buildSectionTypeItemColumnsExpression(sectionTypeIdPath) {
1209
+ const ppeHazardsId = WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.ppeHazards;
1210
+ const hazardRiskPpeId = WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.hazardRiskPpe;
1211
+ const riskMatrixId = WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.riskMatrix;
1212
+ return `{{ context.eval('${sectionTypeIdPath}') === '${riskMatrixId}' ? ['code', 'title', 'matrix'] : [ '${ppeHazardsId}', '${hazardRiskPpeId}' ].includes(context.eval('${sectionTypeIdPath}')) ? ['code', 'ppeItem.title', 'requirementType'] : ['code', 'title'] }}`;
1213
+ }
1214
+ function buildSectionTypeDependentParentFilter(sectionTypeIdPath, parentIdsContextKey, parentSectionTypeKey) {
1215
+ const dependentSectionTypeIds = Object.entries(WORKPLACE_SAFETY_SECTION_TYPE_DEPENDENCIES)
1216
+ .filter(([, config]) => config.kind === 'parentFilter' && config.parentSectionTypeKey === parentSectionTypeKey)
1217
+ .map(([key]) => {
1218
+ const id = Object.entries(WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_BY_ID).find(([, meta]) => meta.sectionTypeKey === key)?.[0];
1219
+ return id ? `'${id}'` : undefined;
1220
+ })
1221
+ .filter((id) => Boolean(id))
1222
+ .join(',');
1223
+ return {
1224
+ field: parentSectionTypeKey === 'Hazard' ? 'hazardId' : 'hazardRiskId',
1225
+ operator: { type: 'in' },
1226
+ value: `{{ [${dependentSectionTypeIds}].includes(context.eval('${sectionTypeIdPath}')) ? ((context.eval('${parentIdsContextKey}') || []).length ? context.eval('${parentIdsContextKey}') : undefined) : undefined }}`,
1227
+ };
1228
+ }
1229
+ function buildSectionTypeDependentHazardFilter(sectionTypeIdPath, parentHazardIdsContextKey) {
1230
+ return buildSectionTypeDependentParentFilter(sectionTypeIdPath, parentHazardIdsContextKey, 'Hazard');
1213
1231
  }
1214
- /**
1215
- * Removes accidental duplicate plan sections.
1216
- * Non-repeatable section types are kept once (first wins); repeatable types may appear more than once.
1217
- * Reassigns contiguous 1-based `sortOrder` to preserve display order.
1218
- */
1219
- function dedupeSafetyPlanSectionStructure(structure, options) {
1220
- const sections = Array.isArray(structure?.sections)
1221
- ? structure.sections
1222
- : [];
1223
- const seenSectionTypeIds = new Set();
1224
- const seenSourceTypePairs = new Set();
1225
- const deduped = [];
1226
- for (const section of sections) {
1227
- const sectionTypeId = resolveEntityRefId(section.sectionTypeId) ?? String(section.sectionTypeId ?? '').trim();
1228
- if (!sectionTypeId) {
1229
- continue;
1230
- }
1231
- const sourceId = String(section.sourceSafetyPlanTypeSectionId ?? '').trim();
1232
- const sourcePairKey = sourceId ? `${sourceId}|${sectionTypeId}` : '';
1233
- // Same type-section source + section type must never appear twice (double materialization).
1234
- if (sourcePairKey && seenSourceTypePairs.has(sourcePairKey)) {
1235
- continue;
1236
- }
1237
- const sectionType = section.sectionType;
1238
- const isRepeatable = isSafetyPlanSectionTypeRepeatable(sectionTypeId, {
1239
- repeatableSectionTypeIds: options?.repeatableSectionTypeIds,
1240
- sectionType,
1241
- });
1242
- if (!isRepeatable && seenSectionTypeIds.has(sectionTypeId)) {
1243
- continue;
1244
- }
1245
- if (sourcePairKey) {
1246
- seenSourceTypePairs.add(sourcePairKey);
1247
- }
1248
- seenSectionTypeIds.add(sectionTypeId);
1249
- deduped.push(section);
1232
+ const sectionItemLookupExpose = [
1233
+ { source: 'id', target: 'items.{id}' },
1234
+ { source: 'title', target: 'items.{title}' },
1235
+ { source: 'code', target: 'items.{code}' },
1236
+ { source: 'ppeItem.id', target: 'items.{id}' },
1237
+ { source: 'ppeItem.title', target: 'items.{title}' },
1238
+ { source: 'ppeItem.code', target: 'items.{code}' },
1239
+ ];
1240
+ /** Shared item lookup options for builder item dialog (`sectionType.*` in dialog context). */
1241
+ function buildSafetyPlanSectionItemLookupOptions(entityExpression, options) {
1242
+ const sectionTypeIdPath = options?.sectionTypeIdPath ?? 'sectionType.id';
1243
+ const lookup = {
1244
+ entity: entityExpression,
1245
+ look: 'lookup',
1246
+ multiple: true,
1247
+ allowSearch: true,
1248
+ textField: buildSectionTypeItemTextFieldExpression(sectionTypeIdPath),
1249
+ columns: buildSectionTypeItemColumnsExpression(sectionTypeIdPath),
1250
+ placeholder: options?.defaultPlaceholderKey ?? defaultItemIdsPlaceholderKey,
1251
+ expose: sectionItemLookupExpose,
1252
+ triggers: WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_SECTION_TYPE_TRIGGERS,
1253
+ };
1254
+ const filters = [];
1255
+ if (options?.parentHazardIdsContextKey) {
1256
+ filters.push(buildSectionTypeDependentHazardFilter(sectionTypeIdPath, options.parentHazardIdsContextKey));
1257
+ }
1258
+ if (options?.parentHazardRiskIdsContextKey) {
1259
+ filters.push(buildSectionTypeDependentParentFilter(sectionTypeIdPath, options.parentHazardRiskIdsContextKey, 'HazardRisk'));
1260
+ }
1261
+ if (filters.length === 1) {
1262
+ lookup['filter'] = filters[0];
1263
+ }
1264
+ else if (filters.length > 1) {
1265
+ lookup['filter'] = { logic: 'and', filters };
1250
1266
  }
1267
+ return lookup;
1268
+ }
1269
+ /** Lookup options with entity data-selector popup UI (`look: 'lookup'`). */
1270
+ function buildSafetyPlanSectionItemSelectorOptions(entityExpression, options) {
1251
1271
  return {
1252
- ...structure,
1253
- sections: deduped.map((section, index) => ({
1254
- ...section,
1255
- sortOrder: index + 1,
1256
- })),
1272
+ ...buildSafetyPlanSectionItemLookupOptions(entityExpression, options),
1273
+ look: 'lookup',
1257
1274
  };
1258
1275
  }
1259
- function collectAllowedSectionTypeIds(typeSections) {
1260
- const ids = new Set();
1261
- for (const typeSection of typeSections) {
1262
- if (typeSection.isArchived === true) {
1263
- continue;
1264
- }
1265
- for (const row of typeSection.sectionTypeIds ?? []) {
1266
- const id = resolveSectionTypeId(row);
1267
- if (id) {
1268
- ids.add(id);
1269
- }
1270
- }
1276
+ function resolveSafetyPlanSectionItemColumns(sectionTypeId) {
1277
+ if (sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.ppeHazards) {
1278
+ return ['code', 'ppeItem.title', 'requirementType'];
1271
1279
  }
1272
- return [...ids];
1280
+ if (sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.hazardRiskPpe) {
1281
+ return ['code', 'hazardRisk.title', 'ppeItem.title', 'requirementType'];
1282
+ }
1283
+ if (sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.riskMatrix) {
1284
+ return [
1285
+ 'code',
1286
+ 'title',
1287
+ 'matrix',
1288
+ { name: 'likelihoodIds', visible: false },
1289
+ { name: 'severityIds', visible: false },
1290
+ { name: 'likelihoodId', visible: false },
1291
+ { name: 'severityId', visible: false },
1292
+ { name: 'matrixOrientation', visible: false },
1293
+ ];
1294
+ }
1295
+ return ['code', 'title'];
1273
1296
  }
1274
- function collectRequiredSourceTypeSectionIds(typeSections) {
1275
- return typeSections
1276
- .filter((row) => row.isArchived !== true && isSafetyPlanTypeSectionDefault(row))
1277
- .map((row) => row.id)
1278
- .filter((id) => typeof id === 'string' && id.length > 0);
1297
+ function resolveSafetyPlanSectionItemEntityKey(sectionTypeId, sectionType) {
1298
+ return resolveSectionItemEntityName(sectionTypeId, sectionType);
1279
1299
  }
1280
- /**
1281
- * Validates that a plan-type section template does not list the same
1282
- * non-repeatable section type more than once (across rows and within a single row's repeater).
1283
- */
1284
- function validateSafetyPlanTypeSectionTemplate(typeSections, repeatableSectionTypeIds = []) {
1285
- const seen = new Map();
1286
- const issues = [];
1287
- for (const typeSection of typeSections) {
1288
- if (typeSection.isArchived === true) {
1289
- continue;
1290
- }
1291
- for (const row of typeSection.sectionTypeIds ?? []) {
1292
- const sectionTypeId = resolveSectionTypeId(row);
1293
- if (!sectionTypeId ||
1294
- isSafetyPlanSectionTypeRepeatable(sectionTypeId, {
1295
- repeatableSectionTypeIds,
1296
- sectionType: row.sectionType,
1297
- })) {
1298
- continue;
1299
- }
1300
- const sectionTypeTitle = resolveSectionTypeTitle(sectionTypeId, row.sectionType);
1301
- if (seen.has(sectionTypeId)) {
1302
- issues.push({
1303
- code: 'INVALID_TYPE_SECTION_TEMPLATE',
1304
- messageKey: '@workplacesafety-management:safety-plans.messages.section-structure.invalid-type-template-duplicate',
1305
- details: {
1306
- sectionTypeId,
1307
- sectionTypeTitle,
1308
- sourceSafetyPlanTypeSectionId: typeSection.id,
1309
- previousSourceSafetyPlanTypeSectionId: seen.get(sectionTypeId)?.sourceSafetyPlanTypeSectionId,
1310
- },
1311
- });
1312
- continue;
1313
- }
1314
- seen.set(sectionTypeId, {
1315
- title: sectionTypeTitle,
1316
- sourceSafetyPlanTypeSectionId: typeSection.id,
1317
- });
1300
+ function mapSafetyPlanSectionSelectedItems(sectionTypeId, selectedItems) {
1301
+ const isPpeHazards = sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.ppeHazards;
1302
+ const isHazardRiskPpe = sectionTypeId === WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS.hazardRiskPpe;
1303
+ return selectedItems
1304
+ .map((item) => {
1305
+ if (isPpeHazards || isHazardRiskPpe) {
1306
+ const ppeItem = item['ppeItem'];
1307
+ const id = String(ppeItem?.['id'] ?? item['id'] ?? '');
1308
+ return {
1309
+ id,
1310
+ title: asSafetyPlanDisplayText(ppeItem?.['title']) || undefined,
1311
+ code: asSafetyPlanDisplayText(ppeItem?.['code']) || undefined,
1312
+ };
1318
1313
  }
1319
- }
1320
- return issues;
1314
+ return {
1315
+ id: String(item['id'] ?? ''),
1316
+ title: asSafetyPlanDisplayText(item['title']) || undefined,
1317
+ code: asSafetyPlanDisplayText(item['code']) || undefined,
1318
+ };
1319
+ })
1320
+ .filter((item) => item.id.length > 0);
1321
1321
  }
1322
- //#endregion
1323
- //#region ---- Validation ----
1324
- function validateSafetyPlanSectionStructure(structure, context) {
1325
- const sections = Array.isArray(structure?.sections) ? structure.sections : [];
1326
- const issues = [];
1327
- const presentSourceIds = new Set(sections
1328
- .map((s) => s.sourceSafetyPlanTypeSectionId)
1329
- .filter((id) => typeof id === 'string' && id.length > 0));
1330
- for (const requiredSourceId of context.requiredSourceTypeSectionIds) {
1331
- if (!presentSourceIds.has(requiredSourceId)) {
1332
- issues.push({
1333
- code: 'REQUIRED_SECTION_MISSING',
1334
- messageKey: '@workplacesafety-management:safety-plans.messages.section-structure.required-section-missing',
1335
- details: { sourceSafetyPlanTypeSectionId: requiredSourceId },
1336
- });
1337
- }
1338
- }
1339
- const allowed = new Set(context.allowedSectionTypeIds);
1340
- const seenSectionTypeIds = new Set();
1341
- const sortOrders = [];
1342
- for (const section of sections) {
1343
- const sectionTypeId = resolveEntityRefId(section.sectionTypeId) ??
1344
- String(section.sectionTypeId ?? '').trim();
1345
- if (!sectionTypeId) {
1322
+ function dedupeSafetyPlanSectionItemSelection(item) {
1323
+ const seen = new Set();
1324
+ const itemIds = [];
1325
+ const items = [];
1326
+ for (let index = 0; index < (item.itemIds?.length ?? 0); index += 1) {
1327
+ const id = item.itemIds?.[index];
1328
+ if (typeof id !== 'string' || !id || seen.has(id)) {
1346
1329
  continue;
1347
1330
  }
1348
- const sectionType = section
1349
- .sectionType;
1350
- const sectionTypeTitle = resolveSectionTypeTitle(sectionTypeId, sectionType);
1351
- if (allowed.size > 0 && !allowed.has(sectionTypeId)) {
1352
- issues.push({
1353
- code: 'SECTION_TYPE_NOT_ALLOWED',
1354
- messageKey: '@workplacesafety-management:safety-plans.messages.section-structure.section-type-not-allowed',
1355
- details: { sectionTypeId, sectionTypeTitle, sectionId: section.id },
1356
- });
1357
- }
1358
- const isRepeatable = isSafetyPlanSectionTypeRepeatable(sectionTypeId, {
1359
- repeatableSectionTypeIds: context.repeatableSectionTypeIds,
1360
- sectionType,
1361
- });
1362
- if (!isRepeatable && seenSectionTypeIds.has(sectionTypeId)) {
1363
- issues.push({
1364
- code: 'DUPLICATE_NON_REPEATABLE_SECTION',
1365
- messageKey: '@workplacesafety-management:safety-plans.messages.section-structure.duplicate-non-repeatable',
1366
- details: { sectionTypeId, sectionTypeTitle, sectionId: section.id },
1367
- });
1368
- }
1369
- seenSectionTypeIds.add(sectionTypeId);
1370
- const sortOrder = section.sortOrder;
1371
- if (typeof sortOrder === 'number' && Number.isFinite(sortOrder)) {
1372
- sortOrders.push(sortOrder);
1373
- }
1374
- }
1375
- if (sortOrders.length !== sections.length) {
1376
- issues.push({
1377
- code: 'INVALID_SORT_ORDER',
1378
- messageKey: '@workplacesafety-management:safety-plans.messages.section-structure.invalid-sort-order',
1331
+ seen.add(id);
1332
+ itemIds.push(id);
1333
+ const snapshot = item.items?.find((entry) => entry?.id === id) ??
1334
+ (item.items?.[index]?.id === id || !item.items?.[index]?.id ? item.items?.[index] : undefined);
1335
+ items.push({
1336
+ id,
1337
+ title: asSafetyPlanDisplayText(snapshot?.title) || undefined,
1338
+ code: asSafetyPlanDisplayText(snapshot?.code) || undefined,
1379
1339
  });
1380
1340
  }
1381
- else {
1382
- const unique = new Set(sortOrders);
1383
- if (unique.size !== sortOrders.length) {
1384
- issues.push({
1385
- code: 'INVALID_SORT_ORDER',
1386
- messageKey: '@workplacesafety-management:safety-plans.messages.section-structure.invalid-sort-order',
1387
- details: { reason: 'duplicate' },
1388
- });
1389
- }
1390
- }
1391
- return issues;
1341
+ return { ...item, itemIds, items };
1392
1342
  }
1393
- function toBuilderValueFromStructure(structure) {
1394
- return {
1395
- sections: structure.sections.map((section) => ({
1396
- id: section.id,
1397
- sectionTypeId: section.sectionTypeId,
1398
- sectionType: section.sectionType,
1399
- title: section.title,
1400
- description: section.description ?? '',
1401
- safetyPlanId: section.safetyPlanId,
1402
- code: section.code,
1403
- sourceSafetyPlanTypeSectionId: section.sourceSafetyPlanTypeSectionId,
1404
- sortOrder: section.sortOrder,
1405
- required: section.required,
1406
- items: section.items.map((item) => ({
1407
- id: item.id,
1408
- itemIds: [...(item.itemIds ?? [])],
1409
- items: item.items ? [...item.items] : [],
1410
- })),
1411
- })),
1412
- };
1343
+ function isPpeRuleSectionTypeId(sectionTypeId) {
1344
+ return isPpeRuleSectionType(sectionTypeId);
1413
1345
  }
1414
1346
  //#endregion
1415
1347
 
1348
+ /** Component key for the safety plan Sections page tab. Register via AXP_PAGE_COMPONENT_PROVIDER. */
1349
+ const SAFETY_PLAN_SECTIONS_PAGE_COMPONENT_KEY = 'safety-plan-sections-page';
1350
+
1351
+ //#region ---- Safety plan section builder value types ----
1352
+ //#endregion
1353
+
1416
1354
  //#region ---- Imports ----
1417
1355
  //#endregion
1418
1356
  //#region ---- Local helpers ----
1419
1357
  function nextId() {
1420
- return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
1421
- ? crypto.randomUUID()
1422
- : `tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1358
+ return createRandomUuid();
1423
1359
  }
1424
1360
  function resolveRefId(value) {
1425
1361
  if (typeof value === 'string') {
@@ -1607,9 +1543,6 @@ var AXMHazardRegisterContextType;
1607
1543
  //#region ---- Imports ----
1608
1544
  //#endregion
1609
1545
 
1610
- //#region ---- Imports ----
1611
- //#endregion
1612
-
1613
1546
  //#endregion
1614
1547
 
1615
1548
  //#region ---- Imports ----
@@ -1642,5 +1575,5 @@ var AXMHazardRegisterContextType;
1642
1575
  * Generated bundle index. Do not edit.
1643
1576
  */
1644
1577
 
1645
- export { AXMHazardPpeRuleRequirementType, AXMHazardRegisterContextType, AXMWorkplaceSafetyManagementCommandKeys, AXMWorkplaceSafetyManagementMenuKeys, AXMWorkplaceSafetyManagementPermissionKeys, AXPSafetyRiskPermissionKeys, PARENT_HAZARD_IDS_CONTEXT_KEY, PARENT_HAZARD_RISK_IDS_CONTEXT_KEY, RISK_MANAGEMENT_HAZARD_CATALOG_SCOPE, RISK_MANAGEMENT_RISK_MATRIX_CELL_ENTITY, RISK_MANAGEMENT_RISK_RATING_ENTITY, RootConfig, SAFETY_PLAN_SECTIONS_PAGE_COMPONENT_KEY, SAFETY_PLAN_SECTION_DUPLICATE_SECTION_TYPE_CODE, SAFETY_PLAN_SECTION_DUPLICATE_SECTION_TYPE_MESSAGE_KEY, WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_BY_ID, WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS, WORKPLACE_SAFETY_BUILT_IN_WORK_STEP_TYPE_IDS, WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD, WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD_RISK, WORKPLACE_SAFETY_GHS_SYMBOL_OPTIONS, WORKPLACE_SAFETY_HAZARD_PPE_RULE_REQUIREMENT_TYPE_OPTIONS, WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_CLEAR_ON_SECTION_TYPE_TRIGGERS, WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_REFRESH_ON_SECTION_TYPE_EXPOSE_TRIGGERS, WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_SECTION_TYPE_TRIGGERS, WORKPLACE_SAFETY_SECTION_TYPE_DEPENDENCIES, WORKPLACE_SAFETY_SECTION_TYPE_DESCRIPTION_SYNC_TRIGGERS, WORKPLACE_SAFETY_SECTION_TYPE_ENTITY_BY_CODE, WORKPLACE_SAFETY_SECTION_TYPE_ENTITY_EXPRESSION, WORKPLACE_SAFETY_SECTION_TYPE_ENTITY_EXPRESSION_SIBLING, WORKPLACE_SAFETY_SECTION_TYPE_ENTITY_FALLBACK, WORKPLACE_SAFETY_SECTION_TYPE_LOOKUP_EXPOSE, WORKPLACE_SAFETY_SECTION_TYPE_LOOKUP_WIDGET_OPTIONS, WORKPLACE_SAFETY_SECTION_TYPE_TITLE_SYNC_TRIGGERS, WORKPLACE_SAFETY_TEMPLATE_TYPE_OPTIONS, buildSafetyPlanSectionItemLookupOptions, buildSafetyPlanSectionItemSelectorOptions, buildSectionStructureFromTypeSections, buildWorkStepContentFromType, clearDependentItemsOnParentChange, collectAllowedSectionTypeIds, collectBuiltInRepeatableSectionTypeIds, collectRequiredSourceTypeSectionIds, createEmptyStepTableCell, createEmptyStepTableRow, dedupeSafetyPlanSectionItemSelection, dedupeSafetyPlanSectionStructure, enrichSectionTypeSnapshot, extractControlMeasureIdsFromHazardRisks, extractRiskMatrixIdsFromHazardRisks, findDuplicateSafetyPlanSectionTypeId, getSectionTypeDependency, isPpeRuleSectionTypeId, isSafetyPlanSectionTypeRepeatable, isSafetyPlanTypeSectionDefault, isStepTableColumnMultiLookup, isStepTableSectionType, mapSafetyPlanSectionSelectedItems, normalizeStepTableColumns, normalizeStepTableRows, resolveEntityRefId, resolveParentHazardIds, resolveParentHazardRiskIds, resolveParentItemIds, resolveSafetyPlanSectionItemColumns, resolveSafetyPlanSectionItemEntityKey, resolveSafetyPlanSectionItemFilter, resolveSafetyPlanSectionItemFilterAsync, resolveSafetyPlanSectionTypeId, resolveSectionItemEntityName, resolveSectionTypeKey, resolveStepTableColumnLookupEntity, safetyPlanSectionTypeAlreadyExists, sectionTypeKeyFromSection, sortStepTableColumns, stepTableColumnKey, stepTableColumnTitle, toBuilderValueFromStructure, toWorkStepBuilderPayload, validateSafetyPlanSectionStructure, validateSafetyPlanTypeSectionTemplate };
1578
+ export { AXMHazardPpeRuleRequirementType, AXMHazardRegisterContextType, AXMWorkplaceSafetyManagementCommandKeys, AXMWorkplaceSafetyManagementMenuKeys, AXMWorkplaceSafetyManagementPermissionKeys, AXPSafetyRiskPermissionKeys, PARENT_HAZARD_IDS_CONTEXT_KEY, PARENT_HAZARD_RISK_IDS_CONTEXT_KEY, RISK_MANAGEMENT_HAZARD_CATALOG_SCOPE, RISK_MANAGEMENT_RISK_MATRIX_CELL_ENTITY, RISK_MANAGEMENT_RISK_RATING_ENTITY, RootConfig, SAFETY_PLAN_SECTIONS_PAGE_COMPONENT_KEY, WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_BY_ID, WORKPLACE_SAFETY_BUILT_IN_SECTION_TYPE_IDS, WORKPLACE_SAFETY_BUILT_IN_WORK_STEP_TYPE_IDS, WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD, WORKPLACE_SAFETY_DOWNSTREAM_FROM_HAZARD_RISK, WORKPLACE_SAFETY_GHS_SYMBOL_OPTIONS, WORKPLACE_SAFETY_HAZARD_PPE_RULE_REQUIREMENT_TYPE_OPTIONS, WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_CLEAR_ON_SECTION_TYPE_TRIGGERS, WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_REFRESH_ON_SECTION_TYPE_EXPOSE_TRIGGERS, WORKPLACE_SAFETY_SECTION_ITEM_LOOKUP_SECTION_TYPE_TRIGGERS, WORKPLACE_SAFETY_SECTION_TYPE_DEPENDENCIES, WORKPLACE_SAFETY_SECTION_TYPE_DESCRIPTION_SYNC_TRIGGERS, WORKPLACE_SAFETY_SECTION_TYPE_ENTITY_BY_CODE, WORKPLACE_SAFETY_SECTION_TYPE_ENTITY_EXPRESSION, WORKPLACE_SAFETY_SECTION_TYPE_ENTITY_EXPRESSION_SIBLING, WORKPLACE_SAFETY_SECTION_TYPE_ENTITY_FALLBACK, WORKPLACE_SAFETY_SECTION_TYPE_LOOKUP_EXPOSE, WORKPLACE_SAFETY_SECTION_TYPE_LOOKUP_WIDGET_OPTIONS, WORKPLACE_SAFETY_SECTION_TYPE_TITLE_SYNC_TRIGGERS, WORKPLACE_SAFETY_TEMPLATE_TYPE_OPTIONS, asSafetyPlanDisplayText, buildSafetyPlanSectionItemLookupOptions, buildSafetyPlanSectionItemSelectorOptions, buildSectionStructureFromTypeSections, buildSectionTypeRelationSnapshot, buildWorkStepContentFromType, clearDependentItemsOnParentChange, collectAllowedSectionTypeIds, collectBuiltInRepeatableSectionTypeIds, collectRequiredSourceTypeSectionIds, createEmptyStepTableCell, createEmptyStepTableRow, dedupeSafetyPlanSectionItemSelection, dedupeSafetyPlanSectionStructure, enrichSectionTypeSnapshot, extractRiskMatrixIdsFromHazardRisks, getSectionTypeDependency, isPpeRuleSectionTypeId, isSafetyPlanSectionTypeRepeatable, isSafetyPlanTypeSectionDefault, isStepTableColumnMultiLookup, isStepTableSectionType, mapSafetyPlanSectionSelectedItems, normalizeStepTableColumns, normalizeStepTableRows, resolveEntityRefId, resolveParentHazardIds, resolveParentHazardRiskIds, resolveParentItemIds, resolveSafetyPlanSectionItemColumns, resolveSafetyPlanSectionItemEntityKey, resolveSafetyPlanSectionItemFilter, resolveSafetyPlanSectionItemFilterAsync, resolveSafetyPlanSectionPersistedTitle, resolveSectionItemEntityName, resolveSectionTypeKey, resolveStepTableColumnLookupEntity, sectionTypeKeyFromSection, sortStepTableColumns, stepTableColumnKey, stepTableColumnTitle, toBuilderValueFromStructure, toWorkStepBuilderPayload, validateSafetyPlanSectionStructure, validateSafetyPlanTypeSectionTemplate };
1646
1579
  //# sourceMappingURL=acorex-platform-module-workplacesafety-management-client-contracts.mjs.map