@jmruthers/pace-core 0.6.10 → 0.6.11

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 (726) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/audit-tool/00-dependencies.cjs +46 -13
  3. package/audit-tool/audits/01-pace-core-compliance.cjs +96 -21
  4. package/audit-tool/audits/02-project-structure.cjs +13 -3
  5. package/audit-tool/audits/03-architecture.cjs +78 -4
  6. package/audit-tool/audits/04-code-quality.cjs +9 -2
  7. package/audit-tool/audits/05-styling.cjs +19 -7
  8. package/audit-tool/audits/06-security-rbac.cjs +105 -14
  9. package/audit-tool/audits/07-api-tech-stack.cjs +31 -15
  10. package/audit-tool/audits/08-testing-documentation.cjs +11 -3
  11. package/audit-tool/audits/09-operations.cjs +19 -7
  12. package/audit-tool/index.cjs +22 -11
  13. package/audit-tool/utils/report-utils.cjs +4 -0
  14. package/cursor-rules/01-pace-core-compliance.mdc +1 -0
  15. package/cursor-rules/02-project-structure.mdc +1 -0
  16. package/cursor-rules/03-architecture.mdc +3 -1
  17. package/cursor-rules/04-code-quality.mdc +1 -0
  18. package/cursor-rules/05-styling.mdc +41 -7
  19. package/cursor-rules/06-security-rbac.mdc +2 -1
  20. package/cursor-rules/07-api-tech-stack.mdc +1 -0
  21. package/cursor-rules/08-testing-documentation.mdc +1 -0
  22. package/cursor-rules/09-operations.mdc +1 -0
  23. package/dist/{DataTable-SAXFG4XI.js → DataTable-EFYP2QLE.js} +10 -7
  24. package/dist/{InactivityServiceProvider-DHryoh6K.d.ts → InactivityServiceProvider-BbxwwDz1.d.ts} +10 -1
  25. package/dist/{UnifiedAuthProvider-CiBAl9-s.d.ts → UnifiedAuthProvider-Bkt_tzdS.d.ts} +56 -24
  26. package/dist/{api-F47QJ7FX.js → api-BZR2CYXL.js} +3 -2
  27. package/dist/api-result-USV1Czr-.d.ts +51 -0
  28. package/dist/{audit-Z6ZZBWLU.js → audit-HI2DHUVU.js} +2 -1
  29. package/dist/{auth-BZOJqrdd.d.ts → auth-JvdRVaud.d.ts} +1 -1
  30. package/dist/{chunk-KSNLMI7N.js → chunk-2DL2WSOE.js} +1 -155
  31. package/dist/{chunk-MPY44PWB.js → chunk-2OEVOGGR.js} +4648 -3560
  32. package/dist/chunk-44CNXN4P.js +15 -0
  33. package/dist/{chunk-Y4PF6HIM.js → chunk-4R3T5ENU.js} +867 -786
  34. package/dist/{chunk-LNHFAF4X.js → chunk-7A6IMHH2.js} +289 -247
  35. package/dist/chunk-CU2BU2MQ.js +2 -0
  36. package/dist/{chunk-JJEYZ3DX.js → chunk-D6BMFMQZ.js} +37 -2
  37. package/dist/{chunk-BCTXBU6U.js → chunk-ENLXB7GP.js} +88 -71
  38. package/dist/{chunk-FBZ7U3ID.js → chunk-J2KQK6DG.js} +937 -987
  39. package/dist/{chunk-TFIPNIPE.js → chunk-KJXRL3XE.js} +3300 -2245
  40. package/dist/{chunk-3GWSPISD.js → chunk-L5LFKKLJ.js} +1 -1
  41. package/dist/{chunk-X5EAU5G7.js → chunk-PCSHBLPB.js} +132 -114
  42. package/dist/{chunk-NIU6DPQV.js → chunk-QRYSEPHB.js} +2 -0
  43. package/dist/{chunk-KYURMOQM.js → chunk-V7FTM2LU.js} +423 -320
  44. package/dist/chunk-WY6Y7KC3.js +264 -0
  45. package/dist/{chunk-FN52B75D.js → chunk-XOJME5T7.js} +176 -15
  46. package/dist/{chunk-7YDC7LMU.js → chunk-XPFVT3GN.js} +71 -66
  47. package/dist/{chunk-66R6RLUZ.js → chunk-YFTFFJIV.js} +3 -3
  48. package/dist/{chunk-W46INAVW.js → chunk-YYTWKVHO.js} +688 -570
  49. package/dist/components.d.ts +8 -7
  50. package/dist/components.js +17 -15
  51. package/dist/{database.generated-DT8JTZiP.d.ts → database.generated-qkdoiVrJ.d.ts} +45 -10
  52. package/dist/eslint-rules/index.cjs +3 -0
  53. package/dist/eslint-rules/rules/03-architecture.cjs +74 -0
  54. package/dist/eslint-rules/rules/06-security-rbac.cjs +74 -0
  55. package/dist/{event-WTAQuGcq.d.ts → event-BfCox3N2.d.ts} +36 -10
  56. package/dist/{file-reference-BavO2eQj.d.ts → file-reference-DU1hcawx.d.ts} +29 -13
  57. package/dist/hooks.d.ts +22 -9
  58. package/dist/hooks.js +34 -25
  59. package/dist/icons/index.d.ts +1 -0
  60. package/dist/icons/index.js +1 -0
  61. package/dist/index.d.ts +66 -177
  62. package/dist/index.js +316 -340
  63. package/dist/pagination-BW1mqywp.d.ts +201 -0
  64. package/dist/providers.d.ts +6 -5
  65. package/dist/providers.js +5 -3
  66. package/dist/rbac/index.d.ts +123 -138
  67. package/dist/rbac/index.js +10 -8
  68. package/dist/theming/runtime.d.ts +19 -2
  69. package/dist/theming/runtime.js +1 -1
  70. package/dist/{timezone-K-ptz3HO.d.ts → timezone-BTWWXKVY.d.ts} +1 -1
  71. package/dist/types.d.ts +17 -10
  72. package/dist/types.js +1 -0
  73. package/dist/{usePublicPageContext-vxBlEHO9.d.ts → usePublicPageContext-B91dGYW1.d.ts} +433 -356
  74. package/dist/{usePublicRouteParams-G3Ks53mk.d.ts → usePublicRouteParams-BgV6VhMi.d.ts} +73 -4
  75. package/dist/utils.d.ts +163 -145
  76. package/dist/utils.js +42 -25
  77. package/docs/api/modules.md +782 -643
  78. package/docs/api-reference/rpc-functions.md +12 -3
  79. package/docs/core-concepts/rbac-system.md +8 -0
  80. package/docs/getting-started/cursor-rules.md +17 -20
  81. package/docs/getting-started/dependencies.md +1 -1
  82. package/docs/getting-started/setup.md +235 -0
  83. package/docs/implementation-guides/authentication.md +27 -0
  84. package/docs/implementation-guides/data-tables.md +176 -3
  85. package/docs/migration/ApiResult-migration.md +25 -0
  86. package/docs/rbac/api-reference.md +33 -31
  87. package/docs/standards/0-standards-overview.md +50 -15
  88. package/docs/standards/1-pace-core-compliance-standards.md +62 -57
  89. package/docs/standards/2-project-structure-standards.md +33 -16
  90. package/docs/standards/3-architecture-standards.md +41 -1
  91. package/docs/standards/4-code-quality-standards.md +26 -6
  92. package/docs/standards/5-styling-standards.md +35 -1
  93. package/docs/standards/6-security-rbac-standards.md +66 -0
  94. package/docs/standards/7-api-tech-stack-standards.md +25 -14
  95. package/docs/standards/8-testing-documentation-standards.md +31 -0
  96. package/docs/standards/9-operations-standards.md +19 -0
  97. package/docs/standards/README.md +20 -201
  98. package/docs/testing/test-setup-for-consumers.md +2 -0
  99. package/docs/troubleshooting/common-issues.md +17 -1
  100. package/docs/troubleshooting/organisation-context-setup.md +8 -0
  101. package/docs/troubleshooting/print-event-name-css-variable-analysis.md +217 -0
  102. package/eslint-config-pace-core.cjs +20 -0
  103. package/package.json +14 -20
  104. package/scripts/{build-docs-incremental.js → build-docs.js} +3 -2
  105. package/scripts/setup.cjs +536 -0
  106. package/scripts/validate.cjs +480 -0
  107. package/src/__tests__/helpers/{__tests__/component-test-utils.test.tsx → component-test-utils.test.tsx} +3 -3
  108. package/src/__tests__/helpers/{__tests__/optimized-test-setup.test.ts → optimized-test-setup.test.ts} +2 -2
  109. package/src/__tests__/helpers/{__tests__/supabaseMock.test.ts → supabaseMock.test.ts} +2 -2
  110. package/src/__tests__/helpers/{__tests__/test-providers.test.tsx → test-providers.test.tsx} +1 -1
  111. package/src/__tests__/helpers/test-providers.tsx +37 -39
  112. package/src/__tests__/helpers/{__tests__/test-utils.test.tsx → test-utils.test.tsx} +4 -3
  113. package/src/__tests__/helpers/{__tests__/timer-utils.test.ts → timer-utils.test.ts} +2 -2
  114. package/src/assets/app-icons/index.test.ts +304 -0
  115. package/src/components/AddressField/AddressField.test.tsx +1 -1
  116. package/src/components/AddressField/AddressField.tsx +238 -212
  117. package/src/components/Button/Button.tsx +1 -1
  118. package/src/components/Card/Card.test.tsx +172 -17
  119. package/src/components/Card/Card.tsx +19 -10
  120. package/src/components/ContextSelector/ContextSelector.internals.tsx +204 -0
  121. package/src/components/ContextSelector/{__tests__/ContextSelector.test.tsx → ContextSelector.test.tsx} +6 -6
  122. package/src/components/ContextSelector/ContextSelector.tsx +66 -280
  123. package/src/components/ContextSelector/ContextSelector.types.ts +35 -0
  124. package/src/components/ContextSelector/useContextSelectorState.tsx +195 -0
  125. package/src/components/DataTable/AUDIT_REPORT.md +59 -44
  126. package/src/components/DataTable/{__tests__/DataTable.comprehensive.test.tsx → DataTable.comprehensive.test.tsx} +6 -6
  127. package/src/components/DataTable/{__tests__/DataTable.default-state.test.tsx → DataTable.default-state.test.tsx} +5 -5
  128. package/src/components/DataTable/{__tests__/DataTable.export.test.tsx → DataTable.export.test.tsx} +10 -10
  129. package/src/components/DataTable/{__tests__/DataTable.grouping-aggregation.test.tsx → DataTable.grouping-aggregation.test.tsx} +6 -6
  130. package/src/components/DataTable/{__tests__/DataTable.hooks.test.tsx → DataTable.hooks.test.tsx} +6 -6
  131. package/src/components/DataTable/{__tests__/DataTable.select-label-display.test.tsx → DataTable.select-label-display.test.tsx} +6 -6
  132. package/src/components/DataTable/DataTable.test.tsx +787 -416
  133. package/src/components/DataTable/DataTable.tsx +12 -12
  134. package/src/components/DataTable/DataTableCore.integration.test.tsx +458 -0
  135. package/src/components/DataTable/{__tests__/DataTableCore.test-setup.ts → DataTableCore.test-setup.ts} +10 -9
  136. package/src/components/DataTable/{__tests__/DataTableCore.test.tsx → DataTableCore.test.tsx} +8 -8
  137. package/src/components/DataTable/{__tests__/README.md → README.md} +17 -7
  138. package/src/components/DataTable/TESTING.md +101 -0
  139. package/src/components/DataTable/{__tests__/a11y.basic.test.tsx → a11y.basic.test.tsx} +34 -34
  140. package/src/components/DataTable/components/DataTableCore.tsx +104 -864
  141. package/src/components/DataTable/components/{__tests__/GroupingDropdown.test.tsx → GroupingDropdown.test.tsx} +17 -8
  142. package/src/components/DataTable/components/GroupingDropdown.tsx +2 -2
  143. package/src/components/DataTable/components/ImportModal.tsx +61 -559
  144. package/src/components/DataTable/components/ImportModalFileSection.tsx +148 -0
  145. package/src/components/DataTable/context/{__tests__/DataTableContext.test.tsx → DataTableContext.test.tsx} +2 -2
  146. package/src/components/DataTable/context/DataTableContext.tsx +7 -6
  147. package/src/components/DataTable/core/{__tests__/ColumnFactory.test.ts → ColumnFactory.test.ts} +2 -2
  148. package/src/components/DataTable/hooks/{__tests__/useColumnOrderPersistence.test.ts → useColumnOrderPersistence.test.ts} +2 -2
  149. package/src/components/DataTable/hooks/{__tests__/useColumnVisibilityPersistence.test.ts → useColumnVisibilityPersistence.test.ts} +2 -2
  150. package/src/components/DataTable/hooks/{__tests__/useDataTableConfiguration.test.ts → useDataTableConfiguration.test.ts} +3 -3
  151. package/src/components/DataTable/hooks/useDataTableConfiguration.ts +14 -2
  152. package/src/components/DataTable/hooks/{__tests__/useDataTableDataPipeline.test.ts → useDataTableDataPipeline.test.ts} +6 -6
  153. package/src/components/DataTable/hooks/useDataTableDeletionBatching.test.ts +127 -0
  154. package/src/components/DataTable/hooks/useDataTableDeletionBatching.ts +106 -0
  155. package/src/components/DataTable/hooks/useDataTableEffectiveActions.test.ts +461 -0
  156. package/src/components/DataTable/hooks/useDataTableEffectiveActions.ts +238 -0
  157. package/src/components/DataTable/hooks/useDataTableLayoutHandlers.test.ts +296 -0
  158. package/src/components/DataTable/hooks/useDataTableLayoutHandlers.ts +175 -0
  159. package/src/components/DataTable/hooks/useDataTablePaginationSync.test.ts +203 -0
  160. package/src/components/DataTable/hooks/useDataTablePaginationSync.ts +109 -0
  161. package/src/components/DataTable/hooks/{__tests__/useDataTablePermissions.test.ts → useDataTablePermissions.test.ts} +11 -11
  162. package/src/components/DataTable/hooks/useDataTablePermissions.ts +79 -247
  163. package/src/components/DataTable/hooks/useDataTablePipeline.test.tsx +219 -0
  164. package/src/components/DataTable/hooks/useDataTablePipeline.tsx +239 -0
  165. package/src/components/DataTable/hooks/useDataTableRenderGuard.test.tsx +316 -0
  166. package/src/components/DataTable/hooks/useDataTableRenderGuard.tsx +195 -0
  167. package/src/components/DataTable/hooks/useDataTableScope.test.ts +110 -0
  168. package/src/components/DataTable/hooks/useDataTableScope.ts +123 -0
  169. package/src/components/DataTable/hooks/{__tests__/useDataTableState.test.ts → useDataTableState.test.ts} +47 -5
  170. package/src/components/DataTable/hooks/useDataTableState.ts +145 -94
  171. package/src/components/DataTable/hooks/useDataTableStateAndPersistence.test.ts +277 -0
  172. package/src/components/DataTable/hooks/useDataTableStateAndPersistence.ts +222 -0
  173. package/src/components/DataTable/hooks/useDataTableSuperAdmin.test.ts +93 -0
  174. package/src/components/DataTable/hooks/useDataTableSuperAdmin.ts +86 -0
  175. package/src/components/DataTable/hooks/useDataTableTableInstance.test.ts +185 -0
  176. package/src/components/DataTable/hooks/useDataTableTableInstance.ts +178 -0
  177. package/src/components/DataTable/hooks/{__tests__/useEffectiveColumnOrder.test.ts → useEffectiveColumnOrder.test.ts} +2 -2
  178. package/src/components/DataTable/hooks/{__tests__/useHierarchicalState.test.ts → useHierarchicalState.test.ts} +2 -2
  179. package/src/components/DataTable/{components/hooks → hooks}/useImportModalFocus.test.ts +3 -3
  180. package/src/components/DataTable/{components/hooks → hooks}/useImportModalFocus.ts +2 -2
  181. package/src/components/DataTable/hooks/useImportModalState.test.ts +390 -0
  182. package/src/components/DataTable/hooks/useImportModalState.ts +345 -0
  183. package/src/components/DataTable/hooks/{__tests__/useKeyboardNavigation.test.ts → useKeyboardNavigation.test.ts} +3 -3
  184. package/src/components/DataTable/hooks/useKeyboardNavigation.ts +309 -269
  185. package/src/components/DataTable/{components/hooks → hooks}/usePermissionTracking.test.ts +3 -3
  186. package/src/components/DataTable/{components/hooks → hooks}/usePermissionTracking.ts +3 -3
  187. package/src/components/DataTable/hooks/{__tests__/useServerSideDataEffect.test.ts → useServerSideDataEffect.test.ts} +2 -2
  188. package/src/components/DataTable/hooks/useServerSideDataEffect.ts +14 -3
  189. package/src/components/DataTable/hooks/{__tests__/useTableColumns.test.ts → useTableColumns.test.ts} +2 -2
  190. package/src/components/DataTable/hooks/{__tests__/useTableHandlers.test.ts → useTableHandlers.test.ts} +25 -4
  191. package/src/components/DataTable/hooks/useTableHandlers.ts +5 -2
  192. package/src/components/DataTable/index.ts +18 -17
  193. package/src/components/DataTable/{__tests__/keyboard.test.tsx → keyboard.test.tsx} +41 -63
  194. package/src/components/DataTable/{__tests__/mocks → mocks}/MockRBACProvider.tsx +1 -1
  195. package/src/components/DataTable/{__tests__/pagination.modes.test.tsx → pagination.modes.test.tsx} +6 -6
  196. package/src/components/DataTable/{__tests__/ssr.strict-mode.test.tsx → ssr.strict-mode.test.tsx} +2 -2
  197. package/src/components/DataTable/{__tests__/styles.test.ts → styles.test.ts} +1 -4
  198. package/src/components/DataTable/styles.ts +0 -1
  199. package/src/components/DataTable/test-utils/MockDataTableComponents.tsx +55 -0
  200. package/src/components/DataTable/{__tests__/test-utils → test-utils}/dataFactories.ts +2 -2
  201. package/src/components/DataTable/test-utils/featureConfig.ts +10 -0
  202. package/src/components/DataTable/{__tests__/test-utils/sharedTestUtils.tsx → test-utils/sharedTestUtils.ts} +97 -66
  203. package/src/components/DataTable/{__tests__/test-utils.ts → test-utils.ts} +1 -1
  204. package/src/components/DataTable/types/actions.ts +71 -0
  205. package/src/components/DataTable/types/base.ts +39 -0
  206. package/src/components/DataTable/types/columns.ts +125 -0
  207. package/src/components/DataTable/types/export.ts +32 -0
  208. package/src/components/DataTable/types/features.ts +81 -0
  209. package/src/components/DataTable/types/hierarchical.ts +44 -0
  210. package/src/components/DataTable/types/index.ts +43 -0
  211. package/src/components/DataTable/types/pagination.ts +85 -0
  212. package/src/components/DataTable/types/performance.ts +47 -0
  213. package/src/components/DataTable/types/props.ts +62 -0
  214. package/src/components/DataTable/types/rbac.ts +45 -0
  215. package/src/components/DataTable/{components/__tests__ → ui/layout}/DataTableCore.test.tsx +430 -28
  216. package/src/components/DataTable/ui/layout/DataTableCore.tsx +345 -0
  217. package/src/components/DataTable/{components/__tests__ → ui/layout}/DataTableErrorBoundary.test.tsx +4 -4
  218. package/src/components/DataTable/{components → ui/layout}/DataTableErrorBoundary.tsx +7 -7
  219. package/src/components/DataTable/ui/layout/DataTableLayout.test.tsx +1352 -0
  220. package/src/components/DataTable/ui/layout/DataTableLayout.tsx +661 -0
  221. package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.test.tsx +91 -0
  222. package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.tsx +43 -0
  223. package/src/components/DataTable/ui/modals/DataTableModals.test.tsx +749 -0
  224. package/src/components/DataTable/{components → ui/modals}/DataTableModals.tsx +36 -28
  225. package/src/components/DataTable/ui/modals/ImportModal.test.tsx +1834 -0
  226. package/src/components/DataTable/ui/modals/ImportModal.tsx +197 -0
  227. package/src/components/DataTable/ui/modals/ImportModalFailedRowsSection.tsx +60 -0
  228. package/src/components/DataTable/ui/modals/ImportModalFileSection.tsx +148 -0
  229. package/src/components/DataTable/ui/modals/ImportModalPreviewSection.tsx +60 -0
  230. package/src/components/DataTable/ui/modals/ImportModalSummarySection.tsx +59 -0
  231. package/src/components/DataTable/ui/modals/importModalPersistence.ts +73 -0
  232. package/src/components/DataTable/{components/__tests__ → ui/shared}/AccessDeniedPage.test.tsx +2 -2
  233. package/src/components/DataTable/{components → ui/shared}/AccessDeniedPage.tsx +2 -2
  234. package/src/components/DataTable/{components/__tests__ → ui/shared}/ActionButtons.test.tsx +6 -4
  235. package/src/components/DataTable/{components → ui/shared}/ActionButtons.tsx +4 -4
  236. package/src/components/DataTable/{components/__tests__ → ui/shared}/ColumnFilter.test.tsx +29 -16
  237. package/src/components/DataTable/{components → ui/shared}/ColumnFilter.tsx +4 -4
  238. package/src/components/DataTable/{components/__tests__ → ui/shared}/PaginationControls.test.tsx +38 -16
  239. package/src/components/DataTable/{components → ui/shared}/PaginationControls.tsx +21 -15
  240. package/src/components/DataTable/{components/__tests__ → ui/shared}/SortIndicator.test.tsx +2 -2
  241. package/src/components/DataTable/{components → ui/shared}/SortIndicator.tsx +1 -1
  242. package/src/components/DataTable/{components/__tests__ → ui/table}/EditFields.test.tsx +3 -3
  243. package/src/components/DataTable/{components → ui/table}/EditFields.tsx +138 -69
  244. package/src/components/DataTable/{components/__tests__ → ui/table}/EditableRow.test.tsx +36 -27
  245. package/src/components/DataTable/{components → ui/table}/EditableRow.tsx +86 -104
  246. package/src/components/DataTable/{components/__tests__ → ui/table}/EmptyState.test.tsx +2 -62
  247. package/src/components/DataTable/{components → ui/table}/EmptyState.tsx +7 -15
  248. package/src/components/DataTable/{components/__tests__ → ui/table}/FilterRow.test.tsx +5 -4
  249. package/src/components/DataTable/{components → ui/table}/FilterRow.tsx +3 -3
  250. package/src/components/DataTable/{components/__tests__ → ui/table}/LoadingState.test.tsx +6 -10
  251. package/src/components/DataTable/{components → ui/table}/LoadingState.tsx +4 -4
  252. package/src/components/DataTable/{components/__tests__ → ui/table}/RowComponent.test.tsx +412 -17
  253. package/src/components/DataTable/{components → ui/table}/RowComponent.tsx +183 -177
  254. package/src/components/DataTable/{components/__tests__ → ui/table}/UnifiedTableBody.test.tsx +425 -16
  255. package/src/components/DataTable/ui/table/UnifiedTableBody.tsx +440 -0
  256. package/src/components/DataTable/{components/__tests__ → ui/table}/cellValueUtils.test.ts +2 -2
  257. package/src/components/DataTable/{components → ui/table}/cellValueUtils.ts +1 -1
  258. package/src/components/DataTable/{components/__tests__ → ui/toolbar}/BulkOperationsDropdown.test.tsx +12 -5
  259. package/src/components/DataTable/{components → ui/toolbar}/BulkOperationsDropdown.tsx +3 -3
  260. package/src/components/DataTable/{components/__tests__ → ui/toolbar}/ColumnVisibilityDropdown.test.tsx +7 -4
  261. package/src/components/DataTable/{components → ui/toolbar}/ColumnVisibilityDropdown.tsx +7 -7
  262. package/src/components/DataTable/{components/__tests__ → ui/toolbar}/DataTableToolbar.test.tsx +4 -4
  263. package/src/components/DataTable/{components → ui/toolbar}/DataTableToolbar.tsx +4 -4
  264. package/src/components/DataTable/ui/toolbar/GroupingDropdown.test.tsx +621 -0
  265. package/src/components/DataTable/ui/toolbar/GroupingDropdown.tsx +107 -0
  266. package/src/components/DataTable/utils/{__tests__/a11yUtils.test.ts → a11yUtils.test.ts} +2 -2
  267. package/src/components/DataTable/utils/{__tests__/aggregationUtils.test.ts → aggregationUtils.test.ts} +3 -3
  268. package/src/components/DataTable/utils/{__tests__/columnUtils.test.ts → columnUtils.test.ts} +2 -2
  269. package/src/components/DataTable/utils/csvParse.test.ts +74 -0
  270. package/src/components/DataTable/utils/csvParse.ts +65 -0
  271. package/src/components/DataTable/utils/{__tests__/errorHandling.test.ts → errorHandling.test.ts} +2 -2
  272. package/src/components/DataTable/utils/{__tests__/exportUtils.test.ts → exportUtils.test.ts} +3 -3
  273. package/src/components/DataTable/utils/{__tests__/flexibleImport.test.ts → flexibleImport.test.ts} +2 -2
  274. package/src/components/DataTable/utils/flexibleImport.ts +3 -186
  275. package/src/components/DataTable/utils/{__tests__/hierarchicalSorting.test.ts → hierarchicalSorting.test.ts} +3 -3
  276. package/src/components/DataTable/utils/{__tests__/hierarchicalUtils.test.ts → hierarchicalUtils.test.ts} +3 -3
  277. package/src/components/DataTable/utils/importDateParser.test.ts +162 -0
  278. package/src/components/DataTable/utils/importDateParser.ts +114 -0
  279. package/src/components/DataTable/utils/importValueParser.test.ts +138 -0
  280. package/src/components/DataTable/utils/importValueParser.ts +91 -0
  281. package/src/components/DataTable/utils/{__tests__/paginationUtils.test.ts → paginationUtils.test.ts} +2 -2
  282. package/src/components/DataTable/utils/paginationUtils.ts +6 -3
  283. package/src/components/DataTable/utils/{__tests__/performanceUtils.test.ts → performanceUtils.test.ts} +3 -3
  284. package/src/components/DataTable/utils/{__tests__/rowUtils.test.ts → rowUtils.test.ts} +3 -3
  285. package/src/components/DataTable/utils/{__tests__/selectFieldUtils.test.ts → selectFieldUtils.test.ts} +66 -3
  286. package/src/components/DataTable/utils/selectFieldUtils.ts +97 -60
  287. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -1
  288. package/src/components/DateTimeField/DateTimeField.test.tsx +1 -1
  289. package/src/components/Dialog/Dialog.test-utils.ts +49 -0
  290. package/src/components/Dialog/Dialog.test.tsx +896 -89
  291. package/src/components/Dialog/Dialog.tsx +174 -882
  292. package/src/components/Dialog/dialogLock.test.ts +238 -0
  293. package/src/components/Dialog/dialogLock.ts +98 -0
  294. package/src/components/Dialog/index.ts +2 -0
  295. package/src/components/Dialog/useDialogDimensions.test.ts +163 -0
  296. package/src/components/Dialog/useDialogDimensions.ts +140 -0
  297. package/src/components/Dialog/useDialogLifecycle.test.ts +358 -0
  298. package/src/components/Dialog/useDialogLifecycle.ts +135 -0
  299. package/src/components/Dialog/useDialogPersistence.test.ts +381 -0
  300. package/src/components/Dialog/useDialogPersistence.ts +357 -0
  301. package/src/components/FileDisplay/FileDisplay.test.tsx +40 -40
  302. package/src/components/FileDisplay/FileDisplay.tsx +24 -656
  303. package/src/components/FileDisplay/FileDisplayContent.test.tsx +395 -0
  304. package/src/components/FileDisplay/FileDisplayContent.tsx +242 -0
  305. package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.test.tsx +74 -0
  306. package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.tsx +38 -0
  307. package/src/components/FileDisplay/FileDisplayEmptyView.test.tsx +33 -0
  308. package/src/components/FileDisplay/FileDisplayEmptyView.tsx +33 -0
  309. package/src/components/FileDisplay/FileDisplayErrorView.test.tsx +71 -0
  310. package/src/components/FileDisplay/FileDisplayErrorView.tsx +50 -0
  311. package/src/components/FileDisplay/FileDisplayLoadingFallbackView.test.tsx +22 -0
  312. package/src/components/FileDisplay/FileDisplayLoadingFallbackView.tsx +22 -0
  313. package/src/components/FileDisplay/FileDisplayLoadingView.test.tsx +21 -0
  314. package/src/components/FileDisplay/FileDisplayLoadingView.tsx +23 -0
  315. package/src/components/FileDisplay/FileDisplayMultipleFilesView.test.tsx +101 -0
  316. package/src/components/FileDisplay/FileDisplayMultipleFilesView.tsx +109 -0
  317. package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.test.tsx +58 -0
  318. package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.tsx +48 -0
  319. package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.test.tsx +111 -0
  320. package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.tsx +270 -0
  321. package/src/components/FileDisplay/FileDisplaySingleImageView.test.tsx +78 -0
  322. package/src/components/FileDisplay/FileDisplaySingleImageView.tsx +67 -0
  323. package/src/components/FileDisplay/fallbackUtils.test.ts +50 -0
  324. package/src/components/FileDisplay/fallbackUtils.ts +44 -0
  325. package/src/components/FileDisplay/fetchFileDisplayData.ts +24 -0
  326. package/src/components/FileDisplay/fetchFileDisplayData.unit.test.ts +183 -0
  327. package/src/components/FileDisplay/fileDisplayUtils.test.ts +58 -0
  328. package/src/components/FileDisplay/fileDisplayUtils.ts +24 -0
  329. package/src/{hooks/__tests__ → components/FileDisplay}/useFileDisplay.test.ts +40 -42
  330. package/src/components/FileDisplay/useFileDisplay.ts +515 -0
  331. package/src/{hooks/__tests__ → components/FileDisplay}/useFileDisplay.unit.test.ts +406 -77
  332. package/src/components/FileDisplay/useFileDisplayData.ts +126 -0
  333. package/src/{hooks/public → components/FileDisplay}/usePublicFileDisplay.test.ts +94 -88
  334. package/src/components/FileDisplay/usePublicFileDisplay.ts +579 -0
  335. package/src/components/FileUpload/FileUpload.test.tsx +16 -10
  336. package/src/components/FileUpload/FileUpload.tsx +107 -525
  337. package/src/components/FileUpload/FileUploadDropZone.tsx +112 -0
  338. package/src/components/FileUpload/FileUploadProgressItem.tsx +86 -0
  339. package/src/components/FileUpload/FileUploadProgressList.tsx +40 -0
  340. package/src/components/FileUpload/useFileUploadManager.test.ts +308 -0
  341. package/src/components/FileUpload/useFileUploadManager.ts +454 -0
  342. package/src/components/FileUpload/useResolvedAppId.test.ts +102 -0
  343. package/src/components/FileUpload/useResolvedAppId.ts +77 -0
  344. package/src/components/Footer/Footer.test.tsx +6 -292
  345. package/src/components/Footer/Footer.tsx +8 -125
  346. package/src/components/Form/Form.test.tsx +44 -27
  347. package/src/components/Form/Form.tsx +64 -287
  348. package/src/components/Form/useFormPersistence.ts +257 -0
  349. package/src/components/Header/Header.test.tsx +17 -18
  350. package/src/components/Header/Header.tsx +10 -1
  351. package/src/components/Input/Input.tsx +1 -1
  352. package/src/components/Label/Label.test.tsx +1 -1
  353. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +1 -1
  354. package/src/components/NavigationMenu/HierarchicalNavItem.tsx +104 -0
  355. package/src/components/NavigationMenu/NavigationMenu.test.tsx +1029 -26
  356. package/src/components/NavigationMenu/NavigationMenu.tsx +61 -361
  357. package/src/components/NavigationMenu/index.ts +6 -1
  358. package/src/components/NavigationMenu/navigationPermissionHelper.ts +188 -0
  359. package/src/components/NavigationMenu/{__tests__/useNavigationFiltering.test.ts → useNavigationFiltering.test.ts} +68 -53
  360. package/src/components/NavigationMenu/useNavigationFiltering.ts +197 -296
  361. package/src/components/NavigationMenu/useNavigationScope.ts +125 -0
  362. package/src/components/PaceAppLayout/PaceAppLayout.edge-cases.test.tsx +77 -62
  363. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +3 -3
  364. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +16 -19
  365. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +529 -5
  366. package/src/components/PaceAppLayout/PaceAppLayout.tsx +280 -756
  367. package/src/components/PaceAppLayout/useFilteredNavItems.ts +304 -0
  368. package/src/components/PaceAppLayout/usePaceAppLayoutConfig.ts +142 -0
  369. package/src/components/PaceAppLayout/usePaceAppLayoutGate.tsx +150 -0
  370. package/src/components/PaceAppLayout/usePaceAppLayoutPermissions.ts +162 -0
  371. package/src/components/PaceAppLayout/usePaceAppLayoutScope.ts +79 -0
  372. package/src/components/PaceAppLayout/useRoleBasedRouteAccess.ts +157 -0
  373. package/src/components/PaceAppLayout/useSuperAdminFallback.ts +58 -0
  374. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +31 -25
  375. package/src/components/PaceLoginPage/PaceLoginPage.tsx +31 -122
  376. package/src/components/PaceLoginPage/useLoginAppAccess.ts +153 -0
  377. package/src/components/Progress/Progress.tsx +1 -2
  378. package/src/components/ProtectedRoute/ProtectedRoute.tsx +29 -235
  379. package/src/components/ProtectedRoute/useProtectedRouteState.ts +128 -0
  380. package/src/components/ProtectedRoute/useVisibilityRedirectGrace.ts +89 -0
  381. package/src/components/PublicLayout/PublicLayout.test.tsx +217 -36
  382. package/src/components/PublicLayout/PublicPageLayout.tsx +132 -73
  383. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -1
  384. package/src/components/Select/Select.test.tsx +1 -1
  385. package/src/components/Select/Select.tsx +28 -18
  386. package/src/components/Select/{__tests__/context.test.tsx → context.test.tsx} +3 -3
  387. package/src/components/Select/{utils/__tests__/text.test.tsx → text.test.tsx} +2 -2
  388. package/src/components/Select/{utils/text.ts → text.ts} +1 -1
  389. package/src/components/Select/{hooks/__tests__/useSelectEvents.test.ts → useSelectEvents.test.ts} +5 -5
  390. package/src/components/Select/{hooks/useSelectEvents.ts → useSelectEvents.ts} +2 -2
  391. package/src/components/Select/{hooks/__tests__/useSelectSearch.test.tsx → useSelectSearch.test.tsx} +7 -7
  392. package/src/components/Select/{hooks/useSelectSearch.ts → useSelectSearch.ts} +2 -2
  393. package/src/components/Select/{hooks/__tests__/useSelectState.test.ts → useSelectState.test.ts} +16 -2
  394. package/src/components/Select/{hooks/useSelectState.ts → useSelectState.ts} +3 -3
  395. package/src/components/Table/Table.test.tsx +348 -0
  396. package/src/components/Tabs/Tabs.test.tsx +270 -0
  397. package/src/components/Tabs/Tabs.tsx +1 -1
  398. package/src/components/Toast/Toast.test.tsx +420 -0
  399. package/src/components/{__tests__/index.test.ts → index.test.ts} +2 -2
  400. package/src/constants/{__tests__/performance.test.ts → performance.test.ts} +2 -2
  401. package/src/hooks/{__tests__/ServiceHooks.test.tsx → ServiceHooks.test.tsx} +8 -8
  402. package/src/hooks/{__tests__/hooks.integration.test.tsx → hooks.integration.test.tsx} +11 -11
  403. package/src/hooks/index.ts +7 -4
  404. package/src/hooks/{__tests__/index.unit.test.ts → index.unit.test.ts} +2 -2
  405. package/src/hooks/public/usePublicEvent.test.ts +1 -1
  406. package/src/hooks/public/usePublicEventLogo.test.ts +1 -1
  407. package/src/hooks/public/usePublicRouteParams.test.ts +1 -1
  408. package/src/hooks/services/useAuth.ts +9 -7
  409. package/src/hooks/useAddressAutocomplete.test.ts +22 -22
  410. package/src/hooks/useAddressAutocomplete.ts +90 -75
  411. package/src/hooks/{__tests__/useAppConfig.unit.test.ts → useAppConfig.unit.test.ts} +328 -22
  412. package/src/hooks/{__tests__/useComponentPerformance.unit.test.tsx → useComponentPerformance.unit.test.tsx} +27 -41
  413. package/src/hooks/useDataTablePerformance.ts +100 -120
  414. package/src/hooks/{__tests__/useDataTablePerformance.unit.test.ts → useDataTablePerformance.unit.test.ts} +5 -5
  415. package/src/hooks/{__tests__/useDataTableState.test.ts → useDataTableState.test.ts} +2 -2
  416. package/src/hooks/{__tests__/useDebounce.unit.test.ts → useDebounce.unit.test.ts} +2 -2
  417. package/src/hooks/useEventTheme.test.ts +4 -1
  418. package/src/hooks/useEventTheme.ts +49 -21
  419. package/src/hooks/useEvents.ts +41 -1
  420. package/src/hooks/{__tests__/useEvents.unit.test.ts → useEvents.unit.test.ts} +5 -5
  421. package/src/hooks/useFileReference.test.ts +44 -41
  422. package/src/hooks/useFileReference.ts +182 -173
  423. package/src/hooks/useFileUrl.ts +1 -1
  424. package/src/hooks/{__tests__/useFileUrl.unit.test.ts → useFileUrl.unit.test.ts} +26 -36
  425. package/src/hooks/{__tests__/useFileUrlCache.test.ts → useFileUrlCache.test.ts} +8 -8
  426. package/src/hooks/useFileUrlCache.ts +1 -1
  427. package/src/hooks/{__tests__/useFocusManagement.unit.test.ts → useFocusManagement.unit.test.ts} +2 -2
  428. package/src/hooks/{__tests__/useFocusTrap.unit.test.tsx → useFocusTrap.unit.test.tsx} +2 -2
  429. package/src/hooks/{__tests__/useFormDialog.test.ts → useFormDialog.test.ts} +2 -2
  430. package/src/hooks/useInactivityTracker.ts +138 -131
  431. package/src/hooks/{__tests__/useInactivityTracker.unit.test.ts → useInactivityTracker.unit.test.ts} +3 -3
  432. package/src/hooks/{__tests__/useIsMobile.unit.test.ts → useIsMobile.unit.test.ts} +2 -2
  433. package/src/hooks/useIsPrint.ts +62 -0
  434. package/src/hooks/useIsPrint.unit.test.ts +545 -0
  435. package/src/hooks/{__tests__/useKeyboardShortcuts.unit.test.ts → useKeyboardShortcuts.unit.test.ts} +2 -2
  436. package/src/hooks/{__tests__/useOrganisationPermissions.unit.test.tsx → useOrganisationPermissions.unit.test.tsx} +4 -4
  437. package/src/hooks/useOrganisationSecurity.test.ts +3 -3
  438. package/src/hooks/useOrganisationSecurity.ts +190 -201
  439. package/src/hooks/{__tests__/useOrganisationSecurity.unit.test.tsx → useOrganisationSecurity.unit.test.tsx} +61 -63
  440. package/src/hooks/{__tests__/useOrganisations.unit.test.ts → useOrganisations.unit.test.ts} +5 -5
  441. package/src/hooks/{__tests__/usePerformanceMonitor.unit.test.ts → usePerformanceMonitor.unit.test.ts} +13 -14
  442. package/src/hooks/{__tests__/usePermissionCache.test.ts → usePermissionCache.test.ts} +26 -27
  443. package/src/hooks/usePermissionCache.ts +276 -271
  444. package/src/hooks/{__tests__/usePreventTabReload.test.ts → usePreventTabReload.test.ts} +2 -2
  445. package/src/hooks/{__tests__/usePublicEvent.simple.test.ts → usePublicEvent.simple.test.ts} +4 -4
  446. package/src/hooks/{__tests__/usePublicEvent.test.ts → usePublicEvent.test.ts} +4 -4
  447. package/src/hooks/{__tests__/usePublicEvent.unit.test.ts → usePublicEvent.unit.test.ts} +4 -4
  448. package/src/hooks/{__tests__/usePublicFileDisplay.test.ts → usePublicFileDisplay.test.ts} +12 -12
  449. package/src/hooks/{__tests__/usePublicRouteParams.unit.test.ts → usePublicRouteParams.unit.test.ts} +3 -3
  450. package/src/hooks/{__tests__/useQueryCache.test.ts → useQueryCache.test.ts} +2 -2
  451. package/src/hooks/useQueryCache.ts +0 -2
  452. package/src/hooks/{__tests__/useRBAC.unit.test.ts → useRBAC.unit.test.ts} +55 -38
  453. package/src/hooks/{__tests__/useSessionDraft.test.ts → useSessionDraft.test.ts} +2 -2
  454. package/src/hooks/{__tests__/useSessionRestoration.unit.test.tsx → useSessionRestoration.unit.test.tsx} +10 -19
  455. package/src/hooks/useStorage.ts +21 -16
  456. package/src/hooks/{__tests__/useStorage.unit.test.ts → useStorage.unit.test.ts} +38 -75
  457. package/src/hooks/{__tests__/useToast.test.ts → useToast.test.ts} +2 -2
  458. package/src/hooks/{__tests__/useToast.unit.test.tsx → useToast.unit.test.tsx} +2 -2
  459. package/src/hooks/{__tests__/useZodForm.unit.test.tsx → useZodForm.unit.test.tsx} +2 -2
  460. package/src/icons/{__tests__/index.test.ts → index.test.ts} +2 -2
  461. package/src/icons/index.ts +2 -0
  462. package/src/{__tests__/index.test.ts → index.test.ts} +3 -7
  463. package/src/index.ts +15 -7
  464. package/src/providers/{__tests__/AuthProvider.test.tsx → AuthProvider.test.tsx} +3 -3
  465. package/src/providers/{__tests__/EventProvider.test.tsx → EventProvider.test.tsx} +3 -3
  466. package/src/providers/InactivityProvider.test-helper.tsx +40 -0
  467. package/src/providers/{__tests__/InactivityProvider.test.tsx → InactivityProvider.test.tsx} +14 -21
  468. package/src/providers/{__tests__/ProviderLifecycle.test.tsx → ProviderLifecycle.test.tsx} +4 -4
  469. package/src/providers/{__tests__/UnifiedAuthProvider.test.tsx → UnifiedAuthProvider.test.tsx} +1 -1
  470. package/src/providers/{__tests__/index.test.ts → index.test.ts} +2 -2
  471. package/src/providers/services/{__tests__/AuthServiceProvider.integration.test.tsx → AuthServiceProvider.integration.test.tsx} +4 -4
  472. package/src/providers/services/{__tests__/AuthServiceProvider.test.tsx → AuthServiceProvider.test.tsx} +7 -7
  473. package/src/providers/services/{__tests__/EventServiceProvider.test.tsx → EventServiceProvider.test.tsx} +7 -7
  474. package/src/providers/services/{__tests__/InactivityServiceProvider.test.tsx → InactivityServiceProvider.test.tsx} +5 -5
  475. package/src/providers/services/{__tests__/OrganisationServiceProvider.test.tsx → OrganisationServiceProvider.test.tsx} +6 -6
  476. package/src/providers/services/UnifiedAuthContext.ts +30 -27
  477. package/src/providers/services/{__tests__/UnifiedAuthProvider.advanced.test.tsx → UnifiedAuthProvider.advanced.test.tsx} +8 -9
  478. package/src/providers/services/{__tests__/UnifiedAuthProvider.appId.test.tsx → UnifiedAuthProvider.appId.test.tsx} +25 -25
  479. package/src/providers/services/{__tests__/UnifiedAuthProvider.integration.test.tsx → UnifiedAuthProvider.integration.test.tsx} +14 -11
  480. package/src/providers/services/UnifiedAuthProvider.tsx +115 -360
  481. package/src/providers/services/{__tests__/contexts.test.tsx → contexts.test.tsx} +6 -6
  482. package/src/providers/services/{__tests__/useUnifiedAuth.test.tsx → useUnifiedAuth.test.tsx} +6 -6
  483. package/src/providers/services/useUnifiedAuthContextValue.ts +279 -0
  484. package/src/providers/useInactivity.test-helper.ts +27 -0
  485. package/src/rbac/{__tests__/adapters.comprehensive.test.tsx → adapters.comprehensive.test.tsx} +24 -24
  486. package/src/rbac/adapters.test.tsx +22 -22
  487. package/src/rbac/adapters.tsx +29 -29
  488. package/src/rbac/api.test.ts +973 -42
  489. package/src/rbac/api.ts +228 -253
  490. package/src/rbac/{__tests__/audit-batched.test.ts → audit-batched.test.ts} +6 -6
  491. package/src/rbac/audit.ts +4 -1
  492. package/src/rbac/{__tests__/auth-rbac-security.integration.test.tsx → auth-rbac-security.integration.test.tsx} +1 -1
  493. package/src/rbac/{__tests__/auth-rbac.e2e.test.tsx → auth-rbac.e2e.test.tsx} +27 -34
  494. package/src/rbac/cache-invalidation.test.ts +715 -0
  495. package/src/rbac/components/{__tests__/AccessDenied.test.tsx → AccessDenied.test.tsx} +3 -3
  496. package/src/rbac/components/{__tests__/NavigationGuard.test.tsx → NavigationGuard.test.tsx} +13 -11
  497. package/src/{__tests__/rbac/PagePermissionGuard.test.tsx → rbac/components/PagePermissionGuard.guard.test.tsx} +33 -19
  498. package/src/rbac/components/{__tests__/PagePermissionGuard.performance.test.tsx → PagePermissionGuard.performance.test.tsx} +30 -9
  499. package/src/rbac/components/{__tests__/PagePermissionGuard.race-condition.test.tsx → PagePermissionGuard.race-condition.test.tsx} +7 -7
  500. package/src/rbac/components/{__tests__/PagePermissionGuard.test.tsx → PagePermissionGuard.test.tsx} +10 -10
  501. package/src/rbac/components/PagePermissionGuard.tsx +177 -372
  502. package/src/rbac/components/{__tests__/PagePermissionGuard.verification.test.tsx → PagePermissionGuard.verification.test.tsx} +7 -7
  503. package/src/rbac/config.ts +58 -18
  504. package/src/rbac/{__tests__/engine.comprehensive.test.ts → engine.comprehensive.test.ts} +3 -3
  505. package/src/rbac/engine.test.ts +494 -0
  506. package/src/rbac/errors.ts +89 -55
  507. package/src/rbac/hooks/permissions/runPermissionCheck.ts +77 -0
  508. package/src/rbac/hooks/permissions/{__tests__/useAccessLevel.test.ts → useAccessLevel.test.ts} +40 -40
  509. package/src/rbac/hooks/permissions/useAccessLevel.ts +16 -6
  510. package/src/rbac/hooks/permissions/{__tests__/useCan.test.ts → useCan.test.ts} +41 -41
  511. package/src/rbac/hooks/permissions/useCan.ts +170 -252
  512. package/src/rbac/hooks/permissions/{__tests__/useMultiplePermissions.test.ts → useMultiplePermissions.test.ts} +49 -49
  513. package/src/rbac/hooks/permissions/useMultiplePermissions.ts +6 -2
  514. package/src/rbac/hooks/permissions/{__tests__/usePermissions.test.ts → usePermissions.test.ts} +10 -12
  515. package/src/rbac/hooks/permissions/usePermissions.ts +36 -65
  516. package/src/rbac/hooks/useCan.test.ts +42 -42
  517. package/src/rbac/hooks/usePageAccessLogging.ts +160 -0
  518. package/src/rbac/hooks/usePageGuardScope.ts +117 -0
  519. package/src/rbac/hooks/usePagePermissionCheck.ts +67 -0
  520. package/src/rbac/hooks/{__tests__/usePermissions.integration.test.ts → usePermissions.integration.test.ts} +9 -9
  521. package/src/{__tests__/hooks/usePermissions.test.ts → rbac/hooks/usePermissions.stability.test.ts} +18 -18
  522. package/src/rbac/hooks/usePermissions.test.ts +54 -54
  523. package/src/rbac/hooks/useRBAC.test.ts +313 -217
  524. package/src/rbac/hooks/useRBAC.ts +145 -81
  525. package/src/rbac/hooks/useResourcePermissions.test.ts +25 -25
  526. package/src/rbac/hooks/useResourcePermissions.ts +68 -134
  527. package/src/rbac/hooks/useResourcePermissionsSuperAdmin.ts +67 -0
  528. package/src/rbac/hooks/useRoleManagement.test.ts +27 -112
  529. package/src/rbac/hooks/useRoleManagement.ts +153 -585
  530. package/src/rbac/hooks/{__tests__/useSecureSupabase.test.ts → useSecureSupabase.test.ts} +17 -17
  531. package/src/rbac/hooks/useSecureSupabase.ts +10 -2
  532. package/src/rbac/hooks/useSuperAdminCheck.ts +80 -0
  533. package/src/rbac/{__tests__/performance.test.ts → performance.test.ts} +1 -1
  534. package/src/rbac/{__tests__/rbac-core.test.tsx → rbac-core.test.tsx} +3 -3
  535. package/src/rbac/{__tests__/rbac-engine-core-logic.test.ts → rbac-engine-core-logic.test.ts} +2 -2
  536. package/src/rbac/{__tests__/rbac-engine-simplified.test.ts → rbac-engine-simplified.test.ts} +3 -3
  537. package/src/rbac/{__tests__/rbac-functions.test.ts → rbac-functions.test.ts} +57 -0
  538. package/src/rbac/{__tests__/rbac-role-isolation.test.ts → rbac-role-isolation.test.ts} +2 -2
  539. package/src/rbac/request-deduplication.test.ts +14 -9
  540. package/src/rbac/request-deduplication.ts +5 -4
  541. package/src/rbac/{__tests__/scenarios.user-role.test.tsx → scenarios.user-role.test.tsx} +23 -23
  542. package/src/rbac/secureClient.test.ts +514 -83
  543. package/src/rbac/secureClient.ts +8 -2
  544. package/src/rbac/security.test.ts +323 -0
  545. package/src/rbac/types/roleManagement.ts +66 -0
  546. package/src/rbac/utils/{__tests__/clientSecurity.test.ts → clientSecurity.test.ts} +4 -4
  547. package/src/rbac/utils/{__tests__/contextValidator.test.ts → contextValidator.test.ts} +4 -4
  548. package/src/rbac/utils/contextValidator.ts +5 -1
  549. package/src/rbac/utils/{__tests__/deep-equal.test.ts → deep-equal.test.ts} +1 -1
  550. package/src/rbac/utils/{__tests__/eventContext.test.ts → eventContext.test.ts} +36 -21
  551. package/src/rbac/utils/eventContext.ts +37 -33
  552. package/src/rbac/utils/fetchPermissionMap.ts +13 -0
  553. package/src/rbac/utils/permissionMapHelpers.ts +34 -0
  554. package/src/rbac/utils/roleManagementRpc.ts +303 -0
  555. package/src/services/{__tests__/AuthService.edge-cases.test.ts → AuthService.edge-cases.test.ts} +19 -19
  556. package/src/services/{__tests__/AuthService.restoreSession.test.ts → AuthService.restoreSession.test.ts} +2 -2
  557. package/src/services/{__tests__/AuthService.test.ts → AuthService.test.ts} +89 -55
  558. package/src/services/AuthService.ts +184 -205
  559. package/src/services/{__tests__/BaseService.edge-cases.test.ts → BaseService.edge-cases.test.ts} +3 -3
  560. package/src/services/{__tests__/BaseService.test.ts → BaseService.test.ts} +2 -2
  561. package/src/services/{__tests__/EventService.edge-cases.test.ts → EventService.edge-cases.test.ts} +27 -24
  562. package/src/services/{__tests__/EventService.eventColours.test.ts → EventService.eventColours.test.ts} +1 -1
  563. package/src/services/{__tests__/EventService.test.ts → EventService.test.ts} +256 -24
  564. package/src/services/EventService.ts +242 -312
  565. package/src/services/{__tests__/InactivityService.edge-cases.test.ts → InactivityService.edge-cases.test.ts} +3 -3
  566. package/src/services/{__tests__/InactivityService.lifecycle.test.ts → InactivityService.lifecycle.test.ts} +2 -2
  567. package/src/services/{__tests__/InactivityService.test.ts → InactivityService.test.ts} +179 -4
  568. package/src/services/InactivityService.ts +172 -213
  569. package/src/services/{__tests__/OrganisationService.edge-cases.test.ts → OrganisationService.edge-cases.test.ts} +5 -5
  570. package/src/services/{__tests__/OrganisationService.pagination.test.ts → OrganisationService.pagination.test.ts} +4 -4
  571. package/src/services/{__tests__/OrganisationService.test.ts → OrganisationService.test.ts} +410 -7
  572. package/src/services/OrganisationService.ts +184 -238
  573. package/src/services/base/BaseService.test.ts +1 -1
  574. package/src/services/interfaces/{__tests__/IAuthService.test.ts → IAuthService.test.ts} +21 -27
  575. package/src/services/interfaces/IAuthService.ts +10 -9
  576. package/src/services/interfaces/{__tests__/IEventService.test.ts → IEventService.test.ts} +4 -4
  577. package/src/services/interfaces/{__tests__/IInactivityService.test.ts → IInactivityService.test.ts} +3 -3
  578. package/src/services/interfaces/{__tests__/IOrganisationService.test.ts → IOrganisationService.test.ts} +3 -3
  579. package/src/styles/core.css +243 -12
  580. package/src/theming/{__tests__/parseEventColours.test.ts → parseEventColours.test.ts} +1 -1
  581. package/src/theming/{__tests__/runtime.test.ts → runtime.test.ts} +8 -17
  582. package/src/theming/runtime.ts +71 -2
  583. package/src/types/api-result.ts +53 -0
  584. package/src/types/{__tests__/core.test.ts → core.test.ts} +2 -2
  585. package/src/types/{__tests__/database-generated.test.ts → database-generated.test.ts} +3 -3
  586. package/src/types/database.generated.ts +45 -10
  587. package/src/types/event.ts +38 -18
  588. package/src/types/{__tests__/file-reference.test.ts → file-reference.test.ts} +13 -13
  589. package/src/types/file-reference.ts +37 -12
  590. package/src/types/{__tests__/guards.test.ts → guards.test.ts} +2 -2
  591. package/src/types/{__tests__/index.test.ts → index.test.ts} +2 -2
  592. package/src/types/index.ts +3 -0
  593. package/src/types/{__tests__/organisation.roles.test.ts → organisation.roles.test.ts} +1 -1
  594. package/src/types/{__tests__/organisation.test.ts → organisation.test.ts} +3 -31
  595. package/src/types/organisation.ts +15 -15
  596. package/src/types/supabase.ts +13 -4
  597. package/src/types/{__tests__/theme.test.ts → theme.test.ts} +1 -1
  598. package/src/types/{__tests__/type-validation.test.ts → type-validation.test.ts} +1 -1
  599. package/src/types/{__tests__/validation.test.ts → validation.test.ts} +2 -2
  600. package/src/utils/app/appIdResolver.test.ts +98 -71
  601. package/src/utils/app/appIdResolver.ts +31 -20
  602. package/src/utils/{__tests__/appConfig.unit.test.ts → appConfig.unit.test.ts} +1 -1
  603. package/src/utils/{__tests__/audit.unit.test.ts → audit.unit.test.ts} +1 -1
  604. package/src/utils/{__tests__/auth-utils.unit.test.ts → auth-utils.unit.test.ts} +16 -17
  605. package/src/utils/{__tests__/bundleAnalysis.unit.test.ts → bundleAnalysis.unit.test.ts} +35 -35
  606. package/src/utils/{__tests__/cn.unit.test.ts → cn.unit.test.ts} +1 -1
  607. package/src/utils/context/organisationContext.test.ts +105 -91
  608. package/src/utils/context/organisationContext.ts +29 -40
  609. package/src/utils/core/{__tests__/cn.test.ts → cn.test.ts} +3 -3
  610. package/src/utils/core/{__tests__/debugLogger.test.ts → debugLogger.test.ts} +2 -2
  611. package/src/utils/core/{__tests__/logger.test.ts → logger.test.ts} +2 -2
  612. package/src/utils/core/mergeRefs.ts +24 -0
  613. package/src/utils/{__tests__/debugLogger.test.ts → debugLogger.test.ts} +1 -1
  614. package/src/utils/{__tests__/deviceFingerprint.unit.test.ts → deviceFingerprint.unit.test.ts} +1 -1
  615. package/src/utils/dynamic/createLazyComponent.tsx +9 -1
  616. package/src/utils/dynamic/{__tests__/dynamicUtils.test.ts → dynamicUtils.test.ts} +2 -2
  617. package/src/utils/dynamic/{__tests__/lazyLoad.test.tsx → lazyLoad.test.tsx} +2 -2
  618. package/src/utils/{__tests__/dynamicUtils.unit.test.ts → dynamicUtils.unit.test.ts} +1 -1
  619. package/src/utils/file-reference/{__tests__/file-reference.test.ts → file-reference.test.ts} +214 -289
  620. package/src/utils/file-reference/index.ts +330 -347
  621. package/src/utils/{__tests__/formatDate.unit.test.ts → formatDate.unit.test.ts} +2 -2
  622. package/src/utils/formatting/formatDateTimeTimezone.test.ts +1 -1
  623. package/src/utils/formatting/formatNumber.test.ts +1 -1
  624. package/src/utils/{__tests__/formatting.unit.test.ts → formatting.unit.test.ts} +1 -1
  625. package/src/utils/google-places/googlePlacesUtils.test.ts +70 -48
  626. package/src/utils/google-places/googlePlacesUtils.ts +67 -99
  627. package/src/utils/google-places/loadGoogleMapsScript.test.ts +25 -22
  628. package/src/utils/google-places/loadGoogleMapsScript.ts +138 -117
  629. package/src/utils/{__tests__/index.unit.test.ts → index.unit.test.ts} +1 -1
  630. package/src/utils/{__tests__/lazyLoad.unit.test.tsx → lazyLoad.unit.test.tsx} +13 -14
  631. package/src/utils/location/location.test.ts +1 -1
  632. package/src/utils/{__tests__/logger.unit.test.ts → logger.unit.test.ts} +1 -1
  633. package/src/utils/{__tests__/organisationContext.unit.test.ts → organisationContext.unit.test.ts} +37 -48
  634. package/src/utils/performance/{__tests__/bundleAnalysis.test.ts → bundleAnalysis.test.ts} +2 -2
  635. package/src/utils/performance/{__tests__/performanceBenchmark.test.ts → performanceBenchmark.test.ts} +2 -2
  636. package/src/utils/performance/{__tests__/performanceBudgets.test.ts → performanceBudgets.test.ts} +2 -2
  637. package/src/utils/{__tests__/performanceBenchmark.test.ts → performanceBenchmark.test.ts} +2 -2
  638. package/src/utils/{__tests__/performanceBudgets.unit.test.ts → performanceBudgets.unit.test.ts} +2 -2
  639. package/src/utils/{__tests__/permissionTypes.unit.test.ts → permissionTypes.unit.test.ts} +1 -1
  640. package/src/utils/{__tests__/permissionUtils.unit.test.ts → permissionUtils.unit.test.ts} +1 -1
  641. package/src/utils/permissions/{__tests__/permissionTypes.test.ts → permissionTypes.test.ts} +2 -2
  642. package/src/utils/persistence/{__tests__/keyDerivation.test.ts → keyDerivation.test.ts} +2 -2
  643. package/src/utils/persistence/{__tests__/sensitiveFieldDetection.test.ts → sensitiveFieldDetection.test.ts} +2 -2
  644. package/src/utils/{__tests__/request-deduplication.test.ts → request-deduplication.test.ts} +2 -2
  645. package/src/utils/{__tests__/sanitization.unit.test.ts → sanitization.unit.test.ts} +1 -1
  646. package/src/utils/{__tests__/schemaUtils.unit.test.ts → schemaUtils.unit.test.ts} +1 -1
  647. package/src/utils/{__tests__/secureDataAccess.unit.test.ts → secureDataAccess.unit.test.ts} +2 -2
  648. package/src/utils/{__tests__/secureErrors.unit.test.ts → secureErrors.unit.test.ts} +4 -4
  649. package/src/utils/{__tests__/secureStorage.unit.test.ts → secureStorage.unit.test.ts} +1 -1
  650. package/src/utils/security/auth-utils.ts +34 -23
  651. package/src/utils/security/secureDataAccess.ts +241 -281
  652. package/src/utils/security/secureErrors.test.ts +1 -1
  653. package/src/utils/security/secureStorage.test.ts +1 -1
  654. package/src/utils/security/security.test.ts +25 -17
  655. package/src/utils/security/security.ts +15 -18
  656. package/src/utils/security/securityMonitor.test.ts +1 -1
  657. package/src/utils/{__tests__/security.unit.test.ts → security.unit.test.ts} +21 -15
  658. package/src/utils/{__tests__/securityMonitor.unit.test.ts → securityMonitor.unit.test.ts} +1 -1
  659. package/src/utils/{__tests__/sessionTracking.unit.test.ts → sessionTracking.unit.test.ts} +12 -12
  660. package/src/utils/storage/{__tests__/config.unit.test.ts → config.unit.test.ts} +2 -2
  661. package/src/utils/storage/helpers.test.ts +88 -102
  662. package/src/utils/storage/helpers.ts +173 -251
  663. package/src/utils/storage/{__tests__/index.unit.test.ts → index.unit.test.ts} +3 -3
  664. package/src/utils/storage/types.ts +7 -0
  665. package/src/utils/supabase/createBaseClient.test.ts +1 -1
  666. package/src/utils/timezone/timezone.test.ts +1 -1
  667. package/src/utils/{__tests__/timezone.test.ts → timezone.test.ts} +2 -2
  668. package/src/utils/validation/{__tests__/common.test.ts → common.test.ts} +2 -2
  669. package/src/utils/validation/{__tests__/csrf.test.ts → csrf.test.ts} +56 -28
  670. package/src/utils/validation/csrf.ts +42 -41
  671. package/src/utils/validation/{__tests__/htmlSanitization.unit.test.ts → htmlSanitization.unit.test.ts} +2 -2
  672. package/src/utils/validation/{__tests__/passwordSchema.test.ts → passwordSchema.test.ts} +2 -2
  673. package/src/utils/validation/{__tests__/schema.test.ts → schema.test.ts} +2 -2
  674. package/src/utils/validation/{__tests__/sqlInjectionProtection.test.ts → sqlInjectionProtection.test.ts} +2 -2
  675. package/src/utils/validation/{__tests__/user.test.ts → user.test.ts} +2 -2
  676. package/src/utils/validation/{__tests__/validation.test.ts → validation.test.ts} +2 -2
  677. package/src/utils/validation/{__tests__/validationUtils.test.ts → validationUtils.test.ts} +2 -2
  678. package/src/utils/{__tests__/validation.unit.test.ts → validation.unit.test.ts} +1 -1
  679. package/src/utils/{__tests__/validationUtils.unit.test.ts → validationUtils.unit.test.ts} +5 -2
  680. package/dist/UnifiedAuthProvider-BBD2PS3Q.js +0 -7
  681. package/dist/chunk-KPYQWGFQ.js +0 -183
  682. package/dist/types-D05dCGma.d.ts +0 -521
  683. package/scripts/eslint-audit.cjs +0 -222
  684. package/scripts/generate-docs.js +0 -157
  685. package/scripts/install-cursor-rules.cjs +0 -255
  686. package/scripts/install-eslint-config.cjs +0 -349
  687. package/scripts/setup-build-cache.js +0 -73
  688. package/scripts/validate-pre-publish.js +0 -145
  689. package/src/__tests__/integration/UserProfile.test.tsx +0 -124
  690. package/src/__tests__/public-recipe-view.test.ts +0 -228
  691. package/src/__tests__/rls-policies.test.ts +0 -472
  692. package/src/components/DataTable/__tests__/DataTable.test.tsx +0 -876
  693. package/src/components/DataTable/components/DataTableLayout.tsx +0 -584
  694. package/src/components/DataTable/components/UnifiedTableBody.tsx +0 -395
  695. package/src/components/DataTable/components/__tests__/DataTableLayout.test.tsx +0 -467
  696. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +0 -358
  697. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +0 -957
  698. package/src/components/DataTable/core/ActionManager.ts +0 -235
  699. package/src/components/DataTable/core/ColumnManager.ts +0 -204
  700. package/src/components/DataTable/core/DataManager.ts +0 -190
  701. package/src/components/DataTable/core/LocalDataAdapter.ts +0 -274
  702. package/src/components/DataTable/core/PluginRegistry.ts +0 -229
  703. package/src/components/DataTable/core/StateManager.ts +0 -312
  704. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +0 -235
  705. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +0 -141
  706. package/src/components/DataTable/core/__tests__/DataManager.test.ts +0 -178
  707. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +0 -133
  708. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +0 -142
  709. package/src/components/DataTable/core/__tests__/StateManager.test.ts +0 -158
  710. package/src/components/DataTable/core/interfaces.ts +0 -338
  711. package/src/components/DataTable/types.ts +0 -764
  712. package/src/hooks/public/usePublicFileDisplay.ts +0 -534
  713. package/src/hooks/useFileDisplay.ts +0 -748
  714. package/src/providers/OrganisationProvider.test.tsx +0 -40
  715. package/src/providers/OrganisationProvider.tsx +0 -92
  716. package/src/providers/__tests__/InactivityProvider.test-helper.tsx +0 -65
  717. package/src/providers/__tests__/OrganisationProvider.test.tsx +0 -616
  718. package/src/providers/__tests__/OrganisationProvider.wrapper.test.tsx +0 -591
  719. package/src/rbac/__tests__/cache-invalidation.test.ts +0 -393
  720. /package/src/components/DataTable/{components/__tests__ → ui}/COVERAGE_NOTE.md +0 -0
  721. /package/src/components/DataTable/utils/{__tests__/COVERAGE_NOTE.md → COVERAGE_NOTE.md} +0 -0
  722. /package/src/hooks/{__tests__/useApiFetch.unit.test.ts → useApiFetch.unit.test.ts} +0 -0
  723. /package/src/providers/{__tests__/README.md → README.md} +0 -0
  724. /package/src/rbac/{__tests__/index.test.ts → index.test.ts} +0 -0
  725. /package/src/rbac/{__tests__/rbac-integration.test.ts → rbac-integration.test.ts} +0 -0
  726. /package/src/types/{__tests__/README.md → README.md} +0 -0
@@ -97,109 +97,184 @@
97
97
  * - Tailwind CSS - Styling
98
98
  */
99
99
 
100
- import React, { useState, useEffect, useMemo } from 'react';
100
+ import React, { useEffect } from 'react';
101
101
  import { Outlet, useNavigate, useLocation } from 'react-router-dom';
102
+ import type { User } from '@supabase/supabase-js';
102
103
  import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
103
104
  import { useOrganisations } from '../../hooks/useOrganisations';
104
- import { useEvents } from '../../hooks/useEvents';
105
+ import { useOptionalEvents } from '../../hooks/useEvents';
105
106
  import { useEventTheme } from '../../hooks/useEventTheme';
106
- import type { Event } from '../../types/event';
107
- import { useCan } from '../../rbac/hooks/usePermissions';
108
- import { useResolvedScope } from '../../rbac/hooks/useResolvedScope';
109
107
  import { useRBAC } from '../../rbac/hooks/useRBAC';
110
- import { isSuperAdmin as checkSuperAdminApi } from '../../rbac/api';
111
108
  import { logger } from '../../utils/core/logger';
112
- import { EventContextRequiredError, OrganisationContextRequiredError, type Permission, type Scope } from '../../rbac/types';
113
- import { AccessDenied } from '../../rbac/components/AccessDenied';
114
-
115
- // Stable empty objects to prevent infinite loops
116
- const EMPTY_PAGE_ID_MAPPING = {};
117
- const EMPTY_ROUTE_PERMISSIONS = {};
118
- import { Button } from '../Button';
109
+ import { useSuperAdminFallback } from './useSuperAdminFallback';
110
+ import { useFilteredNavItems } from './useFilteredNavItems';
111
+ import { useRoleBasedRouteAccess } from './useRoleBasedRouteAccess';
112
+ import { usePaceAppLayoutConfig } from './usePaceAppLayoutConfig';
113
+ import { usePaceAppLayoutPermissions } from './usePaceAppLayoutPermissions';
114
+ import { usePaceAppLayoutGate } from './usePaceAppLayoutGate';
115
+ import { usePaceAppLayoutScope } from './usePaceAppLayoutScope';
119
116
  import { Footer } from '../Footer';
120
117
  import { Header } from '../Header';
121
118
  import type { NavigationItem } from '../NavigationMenu/NavigationMenu';
122
- import { LoadingSpinner } from '../LoadingSpinner';
123
- // Define Operation type locally since old RBAC types are removed
119
+ import { setupPrintDateTime } from '../../theming/runtime';
120
+
124
121
  type Operation = 'read' | 'create' | 'update' | 'delete' | 'manage';
122
+ const EMPTY_PAGE_ID_MAPPING: Record<string, string> = {};
123
+ const EMPTY_ROUTE_PERMISSIONS: Record<string, Operation> = {};
124
+
125
+ /** Route config item for role-based routing. */
126
+ export interface PaceAppLayoutRouteConfigItem {
127
+ path: string;
128
+ component: React.ComponentType;
129
+ permissions: string[];
130
+ roles?: string[];
131
+ accessLevel?: string;
132
+ pageId?: string;
133
+ strictMode?: boolean;
134
+ meta?: {
135
+ title?: string;
136
+ description?: string;
137
+ requiresAuth?: boolean;
138
+ hidden?: boolean;
139
+ };
140
+ }
125
141
 
126
- /**
127
- * Props for the PaceAppLayout component.
128
- * Configures the application layout including navigation, header, and footer.
129
- */
130
- export interface PaceAppLayoutProps {
131
- /** The name of the application to be displayed in the header. */
142
+ /** Optional grouped permission options. When provided, overrides flat permission props. */
143
+ export interface PaceAppLayoutPermissionConfig {
144
+ enforcePermissions?: boolean;
145
+ defaultPermission?: Operation;
146
+ routePermissions?: Record<string, Operation>;
147
+ pageIdMapping?: Record<string, string>;
148
+ permissionFallback?: React.ReactNode;
149
+ strictMode?: boolean;
150
+ enforcePagePermissions?: boolean;
151
+ pagePermissionFallback?: React.ReactNode;
152
+ auditLog?: boolean;
153
+ onPageAccessDenied?: (pageName: string, operation: string) => void;
154
+ onStrictModeViolation?: (pageName: string, operation: string) => void;
155
+ }
156
+
157
+ /** Optional grouped routing options. When provided, overrides flat routing props. */
158
+ export interface PaceAppLayoutRoutingConfig {
159
+ roleBasedRouting?: boolean;
160
+ routeConfig?: PaceAppLayoutRouteConfigItem[];
161
+ fallbackRoute?: string;
162
+ onRouteAccessDenied?: (route: string, reason: string) => void;
163
+ onRouteStrictModeViolation?: (route: string, reason: string) => void;
164
+ }
165
+
166
+ /** Layout props for PaceAppLayout (ISP). */
167
+ export interface PaceAppLayoutPropsLayout {
132
168
  appName: string;
133
- /** Optional navigation items for the header menu. If not provided, uses default navigation. */
134
169
  navItems?: NavigationItem[];
135
- /** Show/hide unified context selector (shows all accessible orgs and events) - default: true */
136
170
  showContextSelector?: boolean;
137
- /** Show organisations in context selector - default: true */
138
171
  showOrganisations?: boolean;
139
- /** Show events in context selector - default: true */
140
172
  showEvents?: boolean;
141
- /** Custom actions to display in the header (between event selector and user menu) */
142
173
  headerActions?: React.ReactNode;
143
- /** URL to navigate to when logo is clicked (defaults to '/dashboard') */
144
174
  logoHref?: string;
145
- /** Custom user menu component (overrides default user menu) */
146
175
  customUserMenu?: React.ReactNode;
147
- /** Custom className for the header */
148
176
  headerClassName?: string;
149
- /** Show/hide user menu */
150
177
  showUserMenu?: boolean;
151
- /** Enable layout-level permission enforcement */
178
+ }
179
+
180
+ /** Permission props for PaceAppLayout (ISP). */
181
+ export interface PaceAppLayoutPropsPermission {
182
+ permissionConfig?: PaceAppLayoutPermissionConfig;
152
183
  enforcePermissions?: boolean;
153
- /** Default permission to check for all routes (when enforcePermissions is true) */
154
184
  defaultPermission?: Operation;
155
- /** Route-specific permissions mapping */
156
185
  routePermissions?: Record<string, Operation>;
157
- /** Fallback component to show when user lacks permission */
158
186
  permissionFallback?: React.ReactNode;
159
- /** Custom permission page ID mapping */
160
187
  pageIdMapping?: Record<string, string>;
161
-
162
- // NEW: Phase 1 - Enhanced Security Features
163
- /** Enable strict mode to prevent bypassing permission checks (default: true) */
164
188
  strictMode?: boolean;
165
- /** Enable page-level permission enforcement (default: false) */
166
189
  enforcePagePermissions?: boolean;
167
- /** Default page permission fallback component */
168
190
  pagePermissionFallback?: React.ReactNode;
169
- /** Enable audit logging for all permission checks (default: true) */
170
191
  auditLog?: boolean;
171
- /** Callback when page access is denied */
172
192
  onPageAccessDenied?: (pageName: string, operation: string) => void;
173
- /** Callback when strict mode violation occurs */
174
193
  onStrictModeViolation?: (pageName: string, operation: string) => void;
175
-
176
- // NEW: Phase 2 - Enhanced Routing Features
177
- /** Enable role-based routing (default: false) */
194
+ }
195
+
196
+ /** Routing props for PaceAppLayout (ISP). */
197
+ export interface PaceAppLayoutPropsRouting {
198
+ routingConfig?: PaceAppLayoutRoutingConfig;
178
199
  roleBasedRouting?: boolean;
179
- /** Route configuration for role-based routing */
180
- routeConfig?: Array<{
181
- path: string;
182
- component: React.ComponentType;
183
- permissions: string[];
184
- roles?: string[];
185
- accessLevel?: string;
186
- pageId?: string;
187
- strictMode?: boolean;
188
- meta?: {
189
- title?: string;
190
- description?: string;
191
- requiresAuth?: boolean;
192
- hidden?: boolean;
193
- };
194
- }>;
195
- /** Fallback route for unauthorized access */
200
+ routeConfig?: PaceAppLayoutRouteConfigItem[];
196
201
  fallbackRoute?: string;
197
- /** Callback when route access is denied */
198
202
  onRouteAccessDenied?: (route: string, reason: string) => void;
199
- /** Callback when route strict mode violation occurs */
200
203
  onRouteStrictModeViolation?: (route: string, reason: string) => void;
201
204
  }
202
205
 
206
+ /**
207
+ * Props for the PaceAppLayout component (ISP: composed from focused interfaces).
208
+ * Permission and routing options can be passed either as flat props or via permissionConfig / routingConfig.
209
+ */
210
+ export type PaceAppLayoutProps = PaceAppLayoutPropsLayout
211
+ & PaceAppLayoutPropsPermission
212
+ & PaceAppLayoutPropsRouting;
213
+
214
+ /** Renders the main layout structure (Header + main + Footer). Extracted to keep PaceAppLayout under max-lines. */
215
+ function PaceAppLayoutContent({
216
+ appName,
217
+ logoHref,
218
+ filteredMenuItems,
219
+ headerActions,
220
+ customUserMenu,
221
+ user,
222
+ onSignOut,
223
+ onChangePassword,
224
+ headerClassName,
225
+ showContextSelector,
226
+ showOrganisations,
227
+ showEvents,
228
+ showUserMenu,
229
+ navigate,
230
+ }: {
231
+ appName: string;
232
+ logoHref: string;
233
+ filteredMenuItems: NavigationItem[];
234
+ headerActions?: React.ReactNode;
235
+ customUserMenu?: React.ReactNode;
236
+ user: User | null | undefined;
237
+ onSignOut: () => Promise<void>;
238
+ onChangePassword: (newPassword: string, confirmPassword: string) => Promise<{ error?: { message: string; code?: string } }>;
239
+ headerClassName?: string;
240
+ showContextSelector: boolean;
241
+ showOrganisations: boolean;
242
+ showEvents: boolean;
243
+ showUserMenu: boolean;
244
+ navigate: (to: string) => void;
245
+ }) {
246
+ return (
247
+ <>
248
+ <Header
249
+ logoUrl={`/${appName.toLowerCase()}_logo_wide.svg`}
250
+ logoAlt={`${appName} Logo`}
251
+ logoHref={logoHref}
252
+ navItems={filteredMenuItems}
253
+ actions={headerActions}
254
+ userMenu={customUserMenu}
255
+ user={user}
256
+ onSignOut={onSignOut}
257
+ onChangePassword={onChangePassword}
258
+ currentPath={window.location.pathname}
259
+ onNavigate={(item) => {
260
+ if (item.href) {
261
+ navigate(item.href);
262
+ }
263
+ }}
264
+ showContextSelector={showContextSelector}
265
+ showOrganisations={showOrganisations}
266
+ showEvents={showEvents}
267
+ showUserMenu={showUserMenu}
268
+ className={headerClassName || "sticky top-0 z-header w-full"}
269
+ />
270
+ <main className="px-4 w-full max-w-app-content-max mx-auto py-8">
271
+ <Outlet />
272
+ </main>
273
+ <Footer />
274
+ </>
275
+ );
276
+ }
277
+
203
278
  /**
204
279
  * A consistent layout component for all PACE suite applications that provides a standard
205
280
  * structure with header, main content area, and footer.
@@ -348,9 +423,9 @@ export interface PaceAppLayoutProps {
348
423
  *
349
424
  * @since 0.1.0
350
425
  */
351
- export function PaceAppLayout({
352
- appName,
353
- navItems,
426
+ export function PaceAppLayout({
427
+ appName,
428
+ navItems,
354
429
  showContextSelector = true,
355
430
  showOrganisations = true,
356
431
  showEvents = true,
@@ -359,734 +434,183 @@ export function PaceAppLayout({
359
434
  customUserMenu,
360
435
  headerClassName,
361
436
  showUserMenu = true,
362
- enforcePermissions = false,
363
- defaultPermission = 'read',
364
- routePermissions = EMPTY_ROUTE_PERMISSIONS,
365
- permissionFallback,
366
- pageIdMapping = EMPTY_PAGE_ID_MAPPING,
367
- // NEW: Phase 1 - Enhanced Security Features
368
- strictMode = true,
369
- enforcePagePermissions = false,
370
- pagePermissionFallback,
371
- auditLog = true,
372
- onPageAccessDenied,
373
- onStrictModeViolation,
374
- // NEW: Phase 2 - Enhanced Routing Features
375
- roleBasedRouting = false,
376
- routeConfig = [],
377
- fallbackRoute = '/unauthorized',
378
- onRouteAccessDenied,
379
- onRouteStrictModeViolation
437
+ permissionConfig,
438
+ enforcePermissions: enforcePermissionsFlat = false,
439
+ defaultPermission: defaultPermissionFlat = 'read',
440
+ routePermissions: routePermissionsFlat = EMPTY_ROUTE_PERMISSIONS,
441
+ permissionFallback: permissionFallbackFlat,
442
+ pageIdMapping: pageIdMappingFlat = EMPTY_PAGE_ID_MAPPING,
443
+ strictMode: strictModeFlat = true,
444
+ enforcePagePermissions: enforcePagePermissionsFlat = false,
445
+ pagePermissionFallback: pagePermissionFallbackFlat,
446
+ auditLog: _auditLogFlat = true,
447
+ onPageAccessDenied: onPageAccessDeniedFlat,
448
+ onStrictModeViolation: onStrictModeViolationFlat,
449
+ routingConfig,
450
+ roleBasedRouting: roleBasedRoutingFlat = false,
451
+ routeConfig: routeConfigFlat = [],
452
+ fallbackRoute: fallbackRouteFlat = '/unauthorized',
453
+ onRouteAccessDenied: onRouteAccessDeniedFlat,
454
+ onRouteStrictModeViolation: onRouteStrictModeViolationFlat,
380
455
  }: PaceAppLayoutProps) {
381
- const { user, signOut, updatePassword, supabase, appId: contextAppId, selectedOrganisationId } = useUnifiedAuth(); // Get appId from context (resolved on login)
382
- const {
383
- selectedOrganisation,
384
- isContextReady: _isContextReady,
385
- hasValidOrganisationContext: _hasValidOrganisationContext,
386
- ensureOrganisationContext: _ensureOrganisationContext,
387
- isLoading: organisationLoading
456
+ const config = usePaceAppLayoutConfig({
457
+ permissionConfig,
458
+ enforcePermissions: enforcePermissionsFlat,
459
+ defaultPermission: defaultPermissionFlat,
460
+ routePermissions: routePermissionsFlat,
461
+ permissionFallback: permissionFallbackFlat,
462
+ pageIdMapping: pageIdMappingFlat,
463
+ strictMode: strictModeFlat,
464
+ enforcePagePermissions: enforcePagePermissionsFlat,
465
+ pagePermissionFallback: pagePermissionFallbackFlat,
466
+ onPageAccessDenied: onPageAccessDeniedFlat,
467
+ onStrictModeViolation: onStrictModeViolationFlat,
468
+ routingConfig,
469
+ roleBasedRouting: roleBasedRoutingFlat,
470
+ routeConfig: routeConfigFlat,
471
+ fallbackRoute: fallbackRouteFlat,
472
+ onRouteAccessDenied: onRouteAccessDeniedFlat,
473
+ onRouteStrictModeViolation: onRouteStrictModeViolationFlat,
474
+ });
475
+
476
+ const { user, signOut, updatePassword, supabase, appId: contextAppId, selectedOrganisationId } = useUnifiedAuth();
477
+ const {
478
+ selectedOrganisation,
479
+ isLoading: organisationLoading,
388
480
  } = useOrganisations();
389
- // Use useRBAC to get super admin status - it's more reliable than async check
390
- // Note: isSuperAdmin might be false initially while loading, but that's OK - we'll allow rendering
391
- // if organisation loading completes or if we're a super admin
392
481
  const { isSuperAdmin: isSuperAdminFromRBAC, isLoading: rbacLoading } = useRBAC();
482
+ const { isSuperAdminDirect, isCheckingSuperAdminDirect } = useSuperAdminFallback(user?.id, isSuperAdminFromRBAC);
393
483
 
394
- // Also check super admin status directly as a fallback (for ADMIN/PORTAL apps)
395
- // This allows super admins to proceed even if RBAC hasn't loaded yet
396
- const [isSuperAdminDirect, setIsSuperAdminDirect] = useState<boolean>(false);
397
- const [isCheckingSuperAdminDirect, setIsCheckingSuperAdminDirect] = useState<boolean>(false);
398
-
484
+ // Update --print-date-time when user prints (e.g. Ctrl+P) so printed pages show current date/time
399
485
  useEffect(() => {
400
- const checkSuperAdminDirect = async () => {
401
- if (!user?.id) {
402
- setIsSuperAdminDirect(false);
403
- setIsCheckingSuperAdminDirect(false);
404
- return;
405
- }
486
+ const teardown = setupPrintDateTime();
487
+ return teardown;
488
+ }, []);
406
489
 
407
- // Only skip if RBAC already confirmed super admin
408
- if (isSuperAdminFromRBAC) {
409
- setIsCheckingSuperAdminDirect(false);
410
- return;
411
- }
412
-
413
- setIsCheckingSuperAdminDirect(true);
414
- try {
415
- const superAdminStatus = await checkSuperAdminApi(user.id);
416
- setIsSuperAdminDirect(superAdminStatus);
417
- } catch (error) {
418
- logger.error('PaceAppLayout', 'Error checking super admin status directly', { userId: user?.id, error });
419
- setIsSuperAdminDirect(false);
420
- } finally {
421
- setIsCheckingSuperAdminDirect(false);
422
- }
423
- };
424
-
425
- checkSuperAdminDirect();
426
- }, [user?.id, isSuperAdminFromRBAC]);
427
-
428
- // Use direct check if RBAC hasn't loaded yet, otherwise use RBAC result
429
490
  const isSuperAdmin = isSuperAdminFromRBAC || isSuperAdminDirect;
430
491
  const navigate = useNavigate();
431
492
  const location = useLocation();
432
-
433
- // Apply event theme colors automatically
493
+
434
494
  useEventTheme();
435
-
436
- // Get selected event (optional)
437
- let selectedEvent: Event | null = null;
438
- try {
439
- const eventsContext = useEvents();
440
- selectedEvent = eventsContext.selectedEvent;
441
- } catch (_error) {
442
- // Event provider not available - continue without event context
443
- }
444
-
445
- // Resolve scope for permission checking
446
- const { resolvedScope, isLoading: scopeLoading } = useResolvedScope({
447
- supabase: supabase || null,
448
- selectedOrganisationId: selectedOrganisation?.id || null,
449
- selectedEventId: selectedEvent?.event_id || null,
450
- selectedEventOrganisationId: selectedEvent?.organisation_id || null
495
+ const { selectedEvent } = useOptionalEvents();
496
+
497
+ const { scope, resolvedAppId, scopeLoading, baseMenuItems } = usePaceAppLayoutScope({
498
+ supabase,
499
+ selectedOrganisation,
500
+ selectedEvent,
501
+ contextAppId,
502
+ navItems,
451
503
  });
452
504
 
453
- // Use appId from context (resolved immediately on login) or fallback to resolvedScope
454
- // This ensures appId is available immediately without waiting for additional resolution
455
- const resolvedAppId = contextAppId || resolvedScope?.appId;
456
-
457
- // Build scope from resolved values
458
- // Preserve appId from resolvedScope or fallback to resolvedAppId
459
- // CRITICAL: Always create a new scope object from primitive values to ensure stable reference
460
- // This prevents useCan from re-checking permissions when resolvedScope changes reference but values are the same
461
- const scopeOrgId = resolvedScope?.organisationId || selectedOrganisation?.id || '';
462
- const scopeEventId = resolvedScope?.eventId || selectedEvent?.event_id || undefined;
463
- const scopeAppId = resolvedScope?.appId || resolvedAppId || undefined;
464
-
465
- const scope = useMemo<Scope>(() => {
466
- const newScope: Scope = {};
467
- if (scopeOrgId) {
468
- newScope.organisationId = scopeOrgId;
469
- }
470
- if (scopeEventId) {
471
- newScope.eventId = scopeEventId;
472
- }
473
- if (scopeAppId) {
474
- newScope.appId = scopeAppId;
475
- }
476
- return newScope;
477
- }, [scopeOrgId, scopeEventId, scopeAppId]);
478
-
479
- // Default navigation items if none provided
480
- const defaultNavItems: NavigationItem[] = useMemo(() => [
481
- { id: 'home', label: 'Home', href: '/', icon: 'Home' },
482
- { id: 'dashboard', label: 'Dashboard', href: '/dashboard', icon: 'LayoutDashboard' },
483
- { id: 'settings', label: 'Settings', href: '/settings', icon: 'Settings' },
484
- { id: 'ui-showcase', label: 'UI Showcase', href: '/ui-showcase', icon: 'Component' },
485
- { id: 'data-table-showcase', label: 'DataTable Showcase', href: '/data-table-showcase', icon: 'Table' },
486
- ], []);
487
-
488
- // Use provided navItems or fall back to default
489
- const baseMenuItems = useMemo(() => navItems || defaultNavItems, [navItems, defaultNavItems]);
490
-
491
- // Get current route permission requirements
492
- const currentRoutePermission = useMemo(() => {
493
- const currentPath = location.pathname;
494
- return routePermissions[currentPath] || defaultPermission;
495
- }, [location.pathname, routePermissions, defaultPermission]);
496
-
497
- // Get current page ID for permission checking
498
- // Extract base page name (first path segment) instead of full route path
499
- // Example: /organisation/scouts-victoria -> "organisation"
500
- const currentPageId = useMemo(() => {
501
- const currentPath = location.pathname;
502
- // Use pageIdMapping if provided (takes precedence)
503
- if (pageIdMapping[currentPath]) {
504
- return pageIdMapping[currentPath];
505
- }
506
- // Extract first path segment (base page name)
507
- const pathSegments = currentPath.slice(1).split('/').filter(Boolean);
508
- // Only return 'home' if there's actually a path segment, otherwise return empty string
509
- // This prevents checking permissions for a non-existent "home" page when the index route is used
510
- return pathSegments[0] || '';
511
- }, [location.pathname, pageIdMapping]);
512
-
513
- // Build permission string in format: operation:page.pageId
514
- const currentPermission = useMemo<Permission>(() => {
515
- // If enforcePermissions is false, don't check any permission (return empty string)
516
- // If currentPageId is empty (index route with no path segments), don't check permissions
517
- if (!enforcePermissions || !currentPageId) {
518
- return '' as Permission;
519
- }
520
- const permissionString = `${currentRoutePermission}:page.${currentPageId}`;
521
- return permissionString as Permission;
522
- }, [enforcePermissions, currentRoutePermission, currentPageId]);
523
-
524
- // Check super admin status before permission enforcement
525
- // Removed duplicate super admin check - using useRBAC hook instead
526
- // The useRBAC hook provides isSuperAdmin which is more reliable
527
-
528
- // Use useCan hook for permission checking (standardized approach)
529
- // Note: The database function already handles super admin bypass, but we check here
530
- // as an additional safety layer to prevent unnecessary permission checks
531
- // Pass appName to useCan so it can be passed to isPermitted for PORTAL/ADMIN special case
532
- // Only check permissions if enforcePermissions is true and we have a valid permission string
533
- const shouldCheckPermission = enforcePermissions && !!currentPermission && !!currentPageId;
534
- // Pass super admin status to avoid duplicate check - use null if still checking, false/true if known
535
- const superAdminStatus = isSuperAdminFromRBAC ? true : (isCheckingSuperAdminDirect ? null : isSuperAdminDirect);
536
- const { can: canFromHook, isLoading: isCheckingPermission, error: permissionError } = useCan(
537
- user?.id || '',
505
+ const permissions = usePaceAppLayoutPermissions({
506
+ config,
507
+ user,
538
508
  scope,
539
- shouldCheckPermission ? currentPermission : ('' as Permission),
540
- shouldCheckPermission ? currentPageId : '',
541
- true, // useCache
542
- superAdminStatus, // Pass super admin status to avoid duplicate check
543
- appName // Pass appName for PORTAL/ADMIN special case
544
- );
545
-
546
- // Permission enforcement state - super admin bypasses all checks
547
- // This ensures super admins never see permission errors even if useCan hasn't completed
548
- // Use combined super admin check (RBAC + direct check)
549
- const can = isSuperAdmin ? true : canFromHook;
550
- const hasPermission = enforcePermissions ? can : true;
551
-
552
- // Check if the permission error is a context error (missing event/org context)
553
- // Context errors should allow the page to render so it can show helpful messaging
554
- // Real permission denials should still show "Access Denied"
555
- const isContextError = useMemo(() => {
556
- if (!permissionError) {
557
- return false;
558
- }
559
- return (
560
- permissionError instanceof EventContextRequiredError ||
561
- permissionError instanceof OrganisationContextRequiredError ||
562
- permissionError.name === 'EventContextRequiredError' ||
563
- permissionError.name === 'OrganisationContextRequiredError'
564
- );
565
- }, [permissionError]);
566
-
567
- // Handle permission check results with audit logging and callbacks
568
- useEffect(() => {
569
- if (!enforcePermissions) {
570
- return;
571
- }
572
-
573
- // Only proceed when permission check is complete (not loading)
574
- // Super admin status is checked via useRBAC hook (isSuperAdminFromRBAC)
575
- // If RBAC is still loading, allow rendering to proceed (optimistic for super admins)
576
- if (isCheckingPermission) {
577
- return;
578
- }
579
-
580
- // NEW: Phase 1 - Enhanced Security Features
581
- // Handle strict mode violations - skip for super admins
582
- if (strictMode && !isSuperAdmin && !can) {
583
- logger.error('PaceAppLayout', 'STRICT MODE VIOLATION: User attempted to access protected page without permission', {
584
- pageName: currentPageId,
585
- operation: currentRoutePermission,
586
- userId: user?.id,
587
- isSuperAdmin: isSuperAdmin,
588
- timestamp: new Date().toISOString()
589
- });
590
-
591
- if (onStrictModeViolation) {
592
- onStrictModeViolation(currentPageId, currentRoutePermission);
593
- }
594
- }
595
-
596
- // Handle page access denied callback - skip for super admins
597
- if (!isSuperAdmin && !can && onPageAccessDenied) {
598
- onPageAccessDenied(currentPageId, currentRoutePermission);
599
- }
600
- }, [enforcePermissions, can, isCheckingPermission, isSuperAdmin, currentPageId, currentRoutePermission, user?.id, strictMode, auditLog, onPageAccessDenied, onStrictModeViolation]);
601
-
602
- // Filter navigation items based on permissions
603
- // Permission filtering is always enabled - users only see navigation items they have permission to access
604
- const [filteredMenuItems, setFilteredMenuItems] = useState<NavigationItem[]>(baseMenuItems);
605
-
606
- useEffect(() => {
607
- let isMounted = true;
608
-
609
- const filterItems = async () => {
610
- // Wait for organisation context to be ready before filtering
611
- // This prevents blocking navigation while context is loading
612
- if (!user?.id) {
613
- // User not loaded yet - show all items until context is ready
614
- if (isMounted) {
615
- setFilteredMenuItems(baseMenuItems);
616
- }
617
- return;
618
- }
619
-
620
- // Use the scope from context (includes selectedOrganisation and selectedEvent)
621
- // This is critical for event-based apps where event context comes from EventService, not user metadata
622
- const currentScope = scope;
623
-
624
- // Wait for scope to be fully resolved (including appId) before filtering
625
- // This is important because getPermissionMap requires appId to fetch pages
626
- // Check both resolvedScope.appId and resolvedAppId (fallback)
627
- // appId is now resolved immediately on login in UnifiedAuthProvider, so it should be available
628
- const _hasAppId = currentScope.appId || resolvedAppId;
629
-
630
- // OPTIMIZATION: Show items optimistically while permissions load
631
- // This makes the menu appear immediately, then we filter when permissions are ready
632
- const hasOrganisationContext = currentScope.organisationId;
633
- const hasUser = !!user?.id;
634
-
635
- // Show navigation optimistically while waiting for context selection.
636
- // Only hide navigation if user is not loaded.
637
- // This prevents navigation from disappearing after initial render while waiting for context.
638
- if (!hasUser) {
639
- // User not loaded yet - show all items until user is ready
640
- if (isMounted) {
641
- setFilteredMenuItems(baseMenuItems);
642
- }
643
- return;
644
- }
645
-
646
- // If no organisation context yet, show items optimistically
647
- // This allows users to see navigation while they select a context
648
- // Only proceed with permission filtering once context is selected
649
- if (!hasOrganisationContext) {
650
- if (isMounted) {
651
- // Show items optimistically when org selector is enabled, otherwise show all items
652
- // This prevents navigation from disappearing while waiting for organisation selection
653
- setFilteredMenuItems(baseMenuItems);
654
- }
655
- return;
656
- }
657
-
658
- // For super admins, show all items (they bypass permission checks)
659
- // Gracefully handle RBAC not being initialized (e.g., in tests)
660
- try {
661
- const { isSuperAdmin: checkSuperAdminDynamic } = await import('../../rbac/api');
662
- const isSuper = await checkSuperAdminDynamic(user.id);
663
-
664
- if (isSuper) {
665
- // Super admins see all navigation items
666
- if (isMounted) {
667
- setFilteredMenuItems(baseMenuItems);
668
- }
669
- return;
670
- }
671
- } catch (error) {
672
- // If RBAC is not initialized (e.g., in tests), continue with normal filtering
673
- // This prevents errors from breaking navigation when RBAC isn't available
674
- if (error && typeof error === 'object' && 'code' in error && error.code === 'RBAC_NOT_INITIALIZED') {
675
- // RBAC not available - proceed with normal filtering without super admin check
676
- // In this case, we'll filter items normally based on permissions
677
- } else {
678
- // Re-throw unexpected errors
679
- throw error;
680
- }
681
- }
682
-
683
- // Organisation context is ready - now filter items based on permissions
684
- // OPTIMIZATION: Use batch permission map instead of individual checks to avoid rate limits
685
- // This makes 1 call instead of N calls (where N = number of navigation items)
686
- // OPTIMIZATION: Proceed even if appId is still resolving - permission checks work with organisationId
687
- try {
688
- const { getPermissionMap } = await import('../../rbac/api');
689
-
690
- // Build scope for permission check - use appId if available, but don't wait for it
691
- // Permission checks will work with resource permissions even without appId
692
- const permissionScope: Scope = {
693
- organisationId: currentScope.organisationId,
694
- eventId: currentScope.eventId,
695
- appId: currentScope.appId || resolvedAppId || undefined // Use appId if available, but proceed without it
696
- };
697
-
698
- const permissionMap = await getPermissionMap({
699
- userId: user.id,
700
- scope: permissionScope,
701
- });
702
-
703
- // Filter items using the permission map and scope type
704
- // First, get scope types for all pages (batch if possible, or individual)
705
- const { getPageScopeType } = await import('../../rbac/api');
706
- const effectiveAppId = currentScope.appId || resolvedAppId;
707
- const effectiveAppName = appName;
708
-
709
- // Determine current context type for scope filtering
710
- const hasEventContext = !!currentScope.eventId;
711
- const hasOrgContext = !!currentScope.organisationId;
712
-
713
- // Filter items using both permission map and scope type
714
- const filtered = await Promise.all(
715
- baseMenuItems.map(async (item) => {
716
- if (!item.href) return { item, hasAccess: true, matchesScope: true, scopeCheckError: false };
717
-
718
- // Extract page ID from href: remove leading slash, fallback to 'dashboard' for root
719
- // This matches database page names in rbac_app_pages
720
- const pageId = pageIdMapping[item.href] || (item.href === '/' ? 'dashboard' : item.href.slice(1)) || 'dashboard';
721
- const permission = routePermissions[item.href] || defaultPermission;
722
- const fullPermission: Permission = permission.includes(':')
723
- ? (permission as Permission)
724
- : (pageId ? `${permission}:page.${pageId}` : permission) as Permission;
725
-
726
- // Check permission map (super admin check already handled in getPermissionMap)
727
- const hasAccess = permissionMap['*'] === true || permissionMap[fullPermission] === true;
728
-
729
- // Check scope type compatibility
730
- let matchesScope = true;
731
- let scopeCheckError = false;
732
- if (effectiveAppId || effectiveAppName) {
733
- try {
734
- const pageScopeType = await getPageScopeType(
735
- pageId,
736
- effectiveAppId,
737
- effectiveAppName
738
- );
739
-
740
- // Filter based on current context:
741
- // - If event is selected: show only 'event' or 'both' pages
742
- // - If only org is selected: show only 'organisation' or 'both' pages
743
- if (hasEventContext) {
744
- // Event context: show 'event' or 'both' pages only
745
- matchesScope = pageScopeType === 'event' || pageScopeType === 'both';
746
- } else if (hasOrgContext) {
747
- // Organisation context only: show 'organisation' or 'both' pages only
748
- matchesScope = pageScopeType === 'organisation' || pageScopeType === 'both';
749
- } else {
750
- // No context: show all pages (shouldn't happen, but safe fallback)
751
- matchesScope = true;
752
- }
753
- } catch (error) {
754
- // If we can't get scope type, allow the page (graceful degradation)
755
- // This prevents navigation from disappearing if there's a database issue
756
- scopeCheckError = true;
757
- logger.warn('PaceAppLayout', 'Failed to get page scope type for navigation filtering', {
758
- pageId,
759
- href: item.href,
760
- contextType: hasEventContext ? 'event' : (hasOrgContext ? 'organisation' : 'none'),
761
- error: error instanceof Error ? error.message : String(error),
762
- note: 'Allowing page to prevent navigation from disappearing - this may indicate missing scope_type in database'
763
- });
764
- matchesScope = true; // Default to allowing if we can't check
765
- }
766
- } else {
767
- // No appId/appName available - can't check scope, allow the page
768
- // This happens during initial load before app context is resolved
769
- matchesScope = true;
770
- }
771
-
772
- return { item, hasAccess, matchesScope, scopeCheckError };
773
- })
774
- );
775
-
776
- if (!isMounted) return;
777
-
778
- const accessibleItems = filtered
779
- .filter(({ hasAccess, matchesScope }) => hasAccess && matchesScope)
780
- .map(({ item }) => item);
781
-
782
- // If all items were filtered out, check if it's due to scope filtering
783
- // This can happen if:
784
- // 1. All pages are scoped for the other context type (expected behavior)
785
- // 2. Scope type lookup failed for all pages (should fallback to showing items with permissions)
786
- if (accessibleItems.length === 0 && filtered.length > 0) {
787
- const itemsWithPermissions = filtered.filter(({ hasAccess }) => hasAccess);
788
- const itemsFilteredByScope = filtered.filter(({ hasAccess, matchesScope }) => hasAccess && !matchesScope);
789
- const itemsWithScopeErrors = filtered.filter(({ hasAccess, scopeCheckError }) => hasAccess && scopeCheckError);
790
-
791
- // If scope checking failed for ALL items with permissions, fall back to showing them
792
- // This prevents navigation from disappearing due to database/API issues
793
- if (itemsWithPermissions.length > 0 &&
794
- itemsWithScopeErrors.length === itemsWithPermissions.length) {
795
- logger.warn('PaceAppLayout', 'Scope checking failed for all items - falling back to permission-only filtering', {
796
- contextType: hasEventContext ? 'event' : (hasOrgContext ? 'organisation' : 'none'),
797
- totalItems: baseMenuItems.length,
798
- itemsWithPermissions: itemsWithPermissions.length,
799
- scopeCheckErrorCount: itemsWithScopeErrors.length,
800
- note: 'Showing items based on permissions only - scope filtering disabled due to errors. This may indicate database issues or missing scope_type values.'
801
- });
802
-
803
- // Fall back to showing items that have permissions (ignore scope check)
804
- const fallbackItems = filtered
805
- .filter(({ hasAccess }) => hasAccess)
806
- .map(({ item }) => item);
807
-
808
- if (isMounted && fallbackItems.length > 0) {
809
- setFilteredMenuItems(fallbackItems);
810
- return;
811
- }
812
- } else if (itemsWithPermissions.length > 0 && itemsFilteredByScope.length === itemsWithPermissions.length) {
813
- // All items were filtered by scope (not errors) - this is expected if all pages are scoped for other context
814
- logger.info('PaceAppLayout', 'All navigation items filtered out by scope type', {
815
- contextType: hasEventContext ? 'event' : (hasOrgContext ? 'organisation' : 'none'),
816
- totalItems: baseMenuItems.length,
817
- itemsWithPermissions: itemsWithPermissions.length,
818
- itemsFilteredByScope: itemsFilteredByScope.length,
819
- note: 'All pages appear to be scoped for a different context type - this is expected behavior. Navigation will be empty until user selects the appropriate context.'
820
- });
821
- }
822
- }
823
-
824
- // SECURITY: Never show all items if permission check fails - this would be a security risk
825
- // If all items are filtered out, it means the user doesn't have permission to see any navigation
826
- // OR all pages are scoped for a different context type
827
- // This is the correct behavior - better to show nothing than show unauthorized items
828
- setFilteredMenuItems(accessibleItems);
829
- } catch (error) {
830
- // On error, fall back to showing all items (graceful degradation)
831
- // This prevents navigation from being empty if permission checks fail
832
- logger.error('PaceAppLayout', 'Failed to load permission map for navigation filtering', { userId: user?.id, error });
833
- if (isMounted) {
834
- setFilteredMenuItems(baseMenuItems);
835
- }
836
- }
837
- };
838
-
839
- filterItems();
509
+ appName,
510
+ isSuperAdminFromRBAC,
511
+ isSuperAdminDirect,
512
+ isCheckingSuperAdminDirect,
513
+ isSuperAdmin,
514
+ });
840
515
 
841
- return () => {
842
- isMounted = false;
843
- };
844
- }, [baseMenuItems, pageIdMapping, routePermissions, defaultPermission, can, user?.id, scope, scopeLoading, contextAppId, resolvedScope?.appId, resolvedAppId, selectedOrganisation?.id, selectedEvent?.event_id, appName]);
516
+ const filteredMenuItems = useFilteredNavItems({
517
+ baseMenuItems,
518
+ user,
519
+ scope,
520
+ routePermissions: config.routePermissions,
521
+ defaultPermission: config.defaultPermission,
522
+ pageIdMapping: config.pageIdMapping,
523
+ appName,
524
+ resolvedAppId,
525
+ scopeLoading,
526
+ contextAppId,
527
+ resolvedScopeAppId: resolvedAppId,
528
+ selectedOrganisationId: selectedOrganisation?.id,
529
+ selectedEventId: selectedEvent?.event_id ?? null,
530
+ });
845
531
 
846
- // NEW: Phase 2 - Enhanced Routing Features
847
- // Check route access for role-based routing
848
- useEffect(() => {
849
- if (!roleBasedRouting || routeConfig.length === 0) return;
850
-
851
- let isMounted = true;
852
-
853
- const checkRouteAccess = async () => {
854
- const currentPath = location.pathname;
855
- const currentRoute = routeConfig.find(route => route.path === currentPath);
856
-
857
- if (!currentRoute) {
858
- // Route not found in configuration
859
- if (strictMode) {
860
- logger.error('PaceAppLayout', 'STRICT MODE VIOLATION: Route not found in configuration', {
861
- route: currentPath,
862
- userId: user?.id,
863
- timestamp: new Date().toISOString()
864
- });
865
-
866
- if (onRouteStrictModeViolation) {
867
- onRouteStrictModeViolation(currentPath, 'Route not found in configuration');
868
- }
869
- }
870
- return;
871
- }
872
-
873
- // Check permissions using useCan hook result
874
- let hasAccess = true; // Default to true if no permission requirements
875
-
876
- // Check page permissions
877
- if (currentRoute.pageId && currentRoute.permissions && currentRoute.permissions.length > 0) {
878
- // Use the permission check result from useCan hook
879
- // For now, we'll use a simple check - in future we might need useMultiplePermissions here
880
- try {
881
- const { isPermittedCached } = await import('../../rbac/api');
882
- const hasPagePermission = await isPermittedCached({
883
- userId: user?.id || '',
884
- scope,
885
- permission: currentRoute.permissions[0] as Permission,
886
- pageId: currentRoute.pageId,
887
- });
888
- if (!isMounted) return;
889
- hasAccess = hasPagePermission;
890
- } catch (error) {
891
- logger.error('PaceAppLayout', 'Failed to check page permission', { route: currentPath, pageId: currentRoute.pageId, error });
892
- if (!isMounted) return;
893
- hasAccess = false;
894
- }
895
- }
896
-
897
- // If permission check passed or not required, check roles
898
- if (hasAccess && currentRoute.roles && currentRoute.roles.length > 0 && user?.id) {
899
- const { useUnifiedAuth } = await import('../../providers/services/UnifiedAuthProvider');
900
- // Note: We're already in the component with authContext via useUnifiedAuth at top
901
- // For this feature to work properly, we need the auth context
902
- // This is a limitation of the current implementation
903
- hasAccess = true; // Will be properly implemented when auth context is available in this effect
904
- }
905
-
906
- if (!isMounted) return;
907
-
908
- if (!hasAccess) {
909
- // Handle route access denied
910
- if (onRouteAccessDenied) {
911
- onRouteAccessDenied(currentPath, 'Insufficient permissions');
912
- }
913
-
914
- if (strictMode) {
915
- logger.error('PaceAppLayout', 'STRICT MODE VIOLATION: User attempted to access protected route without permission', {
916
- route: currentPath,
917
- userId: user?.id,
918
- permissions: currentRoute.permissions,
919
- roles: currentRoute.roles,
920
- accessLevel: currentRoute.accessLevel,
921
- timestamp: new Date().toISOString()
922
- });
923
-
924
- if (onRouteStrictModeViolation) {
925
- onRouteStrictModeViolation(currentPath, 'Insufficient permissions');
926
- }
927
- }
928
-
929
- // Redirect to fallback route
930
- navigate(fallbackRoute, { replace: true });
931
- return;
932
- }
933
- };
934
-
935
- checkRouteAccess();
936
-
937
- return () => {
938
- isMounted = false;
939
- };
940
- }, [roleBasedRouting, routeConfig, location.pathname, strictMode, user?.id, fallbackRoute, scope, navigate, auditLog, onRouteAccessDenied, onRouteStrictModeViolation]);
532
+ useRoleBasedRouteAccess({
533
+ roleBasedRouting: config.roleBasedRouting,
534
+ routeConfig: config.routeConfig,
535
+ currentPath: location.pathname,
536
+ user,
537
+ scope,
538
+ strictMode: config.strictMode,
539
+ fallbackRoute: config.fallbackRoute,
540
+ navigate,
541
+ onRouteAccessDenied: config.onRouteAccessDenied,
542
+ onRouteStrictModeViolation: config.onRouteStrictModeViolation,
543
+ });
941
544
 
942
545
  const handleSignOut = async () => {
943
546
  try {
944
547
  await signOut();
945
548
  } catch (error) {
946
- logger.error('PaceAppLayout', 'Failed to sign out', { error: error instanceof Error ? error.message : String(error) });
549
+ logger.error('PaceAppLayout', 'Failed to sign out', {
550
+ error: error instanceof Error ? error.message : String(error),
551
+ });
947
552
  }
948
553
  };
949
554
 
950
555
  const handleChangePassword = async (newPassword: string, _confirmPassword: string) => {
951
556
  try {
952
- // The form component in UserMenu already checks for matching passwords
953
557
  const result = await updatePassword(newPassword);
954
- if (result?.error) {
955
- // The form will display the error message
558
+ if (!result.ok) {
956
559
  logger.error('PaceAppLayout', 'Failed to change password', { error: result.error.message });
957
- // Convert AuthError to PasswordChangeFormError
958
- return {
959
- error: {
960
- message: result.error.message,
961
- code: result.error.name || 'PASSWORD_UPDATE_ERROR'
962
- }
963
- };
560
+ return { error: { message: result.error.message, code: result.error.code || 'PASSWORD_UPDATE_ERROR' } };
964
561
  }
965
- // The form will handle closing the modal on success
966
562
  return {};
967
563
  } catch (error) {
968
- logger.error('PaceAppLayout', 'Failed to change password', { error: error instanceof Error ? error.message : String(error) });
564
+ logger.error('PaceAppLayout', 'Failed to change password', {
565
+ error: error instanceof Error ? error.message : String(error),
566
+ });
969
567
  return {
970
568
  error: {
971
569
  message: error instanceof Error ? error.message : 'An unexpected error occurred',
972
- code: 'PASSWORD_UPDATE_ERROR'
973
- }
570
+ code: 'PASSWORD_UPDATE_ERROR',
571
+ },
974
572
  };
975
573
  }
976
574
  };
977
575
 
978
- // CRITICAL: Wait for organisation context to be ready before proceeding
979
- // The OrganisationService automatically selects an organisation when loading
980
- // We just need to wait for isContextReady to be true
981
- // No need to call ensureOrganisationContext() here - it will throw if no org is selected
982
- // and the service handles selection automatically during loadUserOrganisations()
983
-
984
- // Show loading state while organisation context is being set
985
- // This is critical - we must wait for organisation context before allowing any data access
986
- // BUT: Allow rendering to proceed if loading is complete, even if user has no organisations (valid state for profile pages)
987
- // Only block if we're actively loading - once loading completes (success or error), allow rendering
988
- // EXCEPTION: Super admins can proceed even during organisation loading (they can access all orgs)
989
- // Use combined super admin check (RBAC + direct check) to allow super admins to proceed immediately
990
- // IMPORTANT: If we're still checking super admin status, allow rendering to proceed (optimistic approach)
991
- // This prevents blocking super admins while their status is being determined
992
- // Also allow rendering if we already have a selectedOrganisationId (even if organisationLoading is still true)
993
- // This prevents blank pages when organisation context is available but loading state hasn't cleared yet
994
- if (user?.id && organisationLoading && !isSuperAdmin && !isCheckingSuperAdminDirect && !rbacLoading && !selectedOrganisationId) {
995
- return (
996
- <p className="grid place-items-center text-center size-full">
997
- <LoadingSpinner
998
- size="lg"/><br />
999
- Loading organisation context...
1000
- </p>
1001
- );
1002
- }
1003
-
1004
- // Once loading is complete (whether success or error), allow rendering to proceed
1005
- // For users without organisations, allow access to profile pages (dashboard, member-profile, medical-profile, additional-contacts)
1006
- // These pages work with user context only and don't require organisation context
1007
- // The app can check hasValidOrganisationContext() to determine if org context is available for org-specific features
1008
-
1009
- // Show loading state while checking permissions
1010
- // Keep loading active until permission check completes to prevent exposing protected content
1011
- // Super admin status is checked via useRBAC hook (isSuperAdminFromRBAC)
1012
- if (enforcePermissions && isCheckingPermission) {
1013
- return (
1014
- <p className="grid place-items-center text-center size-full">
1015
- <LoadingSpinner
1016
- size="lg"/><br />
1017
- Checking permissions...
1018
- </p>
1019
- );
1020
- }
576
+ const { gateContent } = usePaceAppLayoutGate({
577
+ user,
578
+ organisationLoading,
579
+ isSuperAdmin,
580
+ isCheckingSuperAdminDirect,
581
+ rbacLoading,
582
+ selectedOrganisationId,
583
+ enforcePermissions: config.enforcePermissions,
584
+ isCheckingPermission: permissions.isCheckingPermission,
585
+ permissionError: permissions.permissionError,
586
+ isContextError: permissions.isContextError,
587
+ hasPermission: permissions.hasPermission,
588
+ enforcePagePermissions: config.enforcePagePermissions,
589
+ pagePermissionFallback: config.pagePermissionFallback,
590
+ permissionFallback: config.permissionFallback,
591
+ onSignOut: handleSignOut,
592
+ });
1021
593
 
1022
- // Show permission error (only after check is complete)
1023
- // Super admins bypass all permission checks, so don't show errors for them
1024
- // Context errors (missing event/org) should allow the page to render so it can show helpful messaging
1025
- // Real permission errors should still show "Access Denied"
1026
- if (enforcePermissions && permissionError && !isSuperAdmin && !isContextError) {
1027
- return (
1028
- <hgroup className="grid place-items-center text-center size-full">
1029
- <h2>Permission Error</h2>
1030
- <p>{permissionError.message}</p>
1031
- <Button onClick={() => navigate('/')}>Go Home</Button>
1032
- </hgroup>
1033
- );
1034
- }
1035
-
1036
- // Show permission fallback if user lacks permission
1037
- // Only show this if super admin check is complete and user is not a super admin
1038
- // Skip if it's a context error (missing event/org) - allow page to render and show helpful messaging
1039
- if (enforcePermissions && hasPermission === false && !isCheckingSuperAdminDirect && !isSuperAdmin && !isContextError) {
1040
- // NEW: Phase 1 - Use page permission fallback if available
1041
- if (enforcePagePermissions && pagePermissionFallback) {
1042
- return <>{pagePermissionFallback}</>;
1043
- }
1044
-
1045
- if (permissionFallback) {
1046
- return <>{permissionFallback}</>;
1047
- }
1048
-
1049
- return (
1050
- <AccessDenied
1051
- message="You don't have permission to access this page."
1052
- onGoBack={() => navigate('/')}
1053
- onSignOut={async () => {
1054
- await handleSignOut();
1055
- navigate('/login');
1056
- }}
1057
- showSignOut={true}
1058
- />
1059
- );
594
+ if (gateContent !== null) {
595
+ return <>{gateContent}</>;
1060
596
  }
1061
597
 
1062
598
  return (
1063
- <>
1064
- <Header
1065
- logoUrl={`/${appName.toLowerCase()}_logo_wide.svg`}
1066
- logoAlt={`${appName} Logo`}
1067
- logoHref={logoHref}
1068
- navItems={filteredMenuItems}
1069
- actions={headerActions}
1070
- userMenu={customUserMenu}
1071
- user={user}
1072
- onSignOut={handleSignOut}
1073
- onChangePassword={handleChangePassword}
1074
- currentPath={window.location.pathname}
1075
- onNavigate={(item) => {
1076
- if (item.href) {
1077
- navigate(item.href);
1078
- }
1079
- }}
1080
- showContextSelector={showContextSelector}
1081
- showOrganisations={showOrganisations}
1082
- showEvents={showEvents}
1083
- showUserMenu={showUserMenu}
1084
- className={headerClassName || "sticky top-0 z-[40] w-full"}
1085
- />
1086
- <main className="px-4 w-[min(var(--app-width),100%)] mx-auto py-8">
1087
- <Outlet />
1088
- </main>
1089
- <Footer />
1090
- </>
599
+ <PaceAppLayoutContent
600
+ appName={appName}
601
+ logoHref={logoHref}
602
+ filteredMenuItems={filteredMenuItems}
603
+ headerActions={headerActions}
604
+ customUserMenu={customUserMenu}
605
+ user={user}
606
+ onSignOut={handleSignOut}
607
+ onChangePassword={handleChangePassword}
608
+ headerClassName={headerClassName}
609
+ showContextSelector={showContextSelector}
610
+ showOrganisations={showOrganisations}
611
+ showEvents={showEvents}
612
+ showUserMenu={showUserMenu}
613
+ navigate={navigate}
614
+ />
1091
615
  );
1092
616
  }