@jmruthers/pace-core 0.5.67 → 0.5.69

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 (384) hide show
  1. package/README.md +26 -0
  2. package/dist/{DataTable-MFUXNGPR.js → DataTable-MPBSXUC6.js} +5 -6
  3. package/dist/{PublicLoadingSpinner-DdKXTkCZ.d.ts → PublicLoadingSpinner-BOdyU3u-.d.ts} +1 -1
  4. package/dist/{UnifiedAuthProvider-CQNiemcB.d.ts → UnifiedAuthProvider-D02AMXgO.d.ts} +3 -3
  5. package/dist/{chunk-CKNY7HYS.js → chunk-2ARQW6VX.js} +3 -3
  6. package/dist/{chunk-T2MQY57J.js → chunk-6JILXFEA.js} +335 -5
  7. package/dist/chunk-6JILXFEA.js.map +1 -0
  8. package/dist/{chunk-D7ARGIA3.js → chunk-6RBH67W7.js} +23 -6
  9. package/dist/chunk-6RBH67W7.js.map +1 -0
  10. package/dist/{chunk-C7GUF747.js → chunk-FJTAWPAQ.js} +3 -5
  11. package/dist/{chunk-C7GUF747.js.map → chunk-FJTAWPAQ.js.map} +1 -1
  12. package/dist/{chunk-4HQ5BOVZ.js → chunk-NO5QHMDX.js} +7 -6
  13. package/dist/chunk-NO5QHMDX.js.map +1 -0
  14. package/dist/{chunk-ZPK5656W.js → chunk-O3NWNXDY.js} +4 -5
  15. package/dist/chunk-O3NWNXDY.js.map +1 -0
  16. package/dist/{chunk-BTCA3ENN.js → chunk-Q2UP3ZWQ.js} +4 -4
  17. package/dist/{chunk-QVEOQVD4.js → chunk-RVYGJPOD.js} +173 -20
  18. package/dist/chunk-RVYGJPOD.js.map +1 -0
  19. package/dist/{chunk-FVDOEGGG.js → chunk-UCMHBF7Y.js} +3 -5
  20. package/dist/{chunk-FVDOEGGG.js.map → chunk-UCMHBF7Y.js.map} +1 -1
  21. package/dist/{chunk-T6HVDA24.js → chunk-V3QO3LL7.js} +5 -7
  22. package/dist/chunk-V3QO3LL7.js.map +1 -0
  23. package/dist/{chunk-ZB6AEA7I.js → chunk-ZXJGZLLO.js} +17 -17
  24. package/dist/{chunk-ZB6AEA7I.js.map → chunk-ZXJGZLLO.js.map} +1 -1
  25. package/dist/components.d.ts +2 -2
  26. package/dist/components.js +8 -9
  27. package/dist/components.js.map +1 -1
  28. package/dist/hooks.d.ts +1 -1
  29. package/dist/hooks.js +9 -6
  30. package/dist/hooks.js.map +1 -1
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.js +16 -16
  33. package/dist/index.js.map +1 -1
  34. package/dist/providers.d.ts +1 -1
  35. package/dist/providers.js +5 -7
  36. package/dist/rbac/index.js +5 -6
  37. package/dist/{usePublicRouteParams-CdoFxnJK.d.ts → usePublicRouteParams-Ua1Vz-HG.d.ts} +35 -1
  38. package/dist/utils.d.ts +4 -1
  39. package/dist/utils.js +3 -3
  40. package/docs/DOCUMENTATION_CHECKLIST.md +281 -0
  41. package/docs/README.md +22 -10
  42. package/docs/api/README.md +26 -0
  43. package/docs/api/classes/ColumnFactory.md +1 -1
  44. package/docs/api/classes/ErrorBoundary.md +1 -1
  45. package/docs/api/classes/InvalidScopeError.md +1 -1
  46. package/docs/api/classes/MissingUserContextError.md +1 -1
  47. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  48. package/docs/api/classes/PermissionDeniedError.md +1 -1
  49. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  50. package/docs/api/classes/RBACAuditManager.md +1 -1
  51. package/docs/api/classes/RBACCache.md +1 -1
  52. package/docs/api/classes/RBACEngine.md +1 -1
  53. package/docs/api/classes/RBACError.md +1 -1
  54. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  55. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  56. package/docs/api/classes/StorageUtils.md +1 -1
  57. package/docs/api/interfaces/AggregateConfig.md +1 -1
  58. package/docs/api/interfaces/ButtonProps.md +1 -1
  59. package/docs/api/interfaces/CardProps.md +1 -1
  60. package/docs/api/interfaces/ColorPalette.md +1 -1
  61. package/docs/api/interfaces/ColorShade.md +1 -1
  62. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  63. package/docs/api/interfaces/DataTableAction.md +1 -1
  64. package/docs/api/interfaces/DataTableColumn.md +1 -1
  65. package/docs/api/interfaces/DataTableProps.md +1 -1
  66. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  67. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  68. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  69. package/docs/api/interfaces/EventContextType.md +1 -1
  70. package/docs/api/interfaces/EventLogoProps.md +1 -1
  71. package/docs/api/interfaces/EventProviderProps.md +1 -1
  72. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  73. package/docs/api/interfaces/FileUploadProps.md +1 -1
  74. package/docs/api/interfaces/FooterProps.md +1 -1
  75. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  76. package/docs/api/interfaces/InputProps.md +1 -1
  77. package/docs/api/interfaces/LabelProps.md +1 -1
  78. package/docs/api/interfaces/LoginFormProps.md +1 -1
  79. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  80. package/docs/api/interfaces/NavigationContextType.md +1 -1
  81. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  82. package/docs/api/interfaces/NavigationItem.md +1 -1
  83. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  84. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  85. package/docs/api/interfaces/Organisation.md +1 -1
  86. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  87. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  88. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  89. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  90. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  91. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  92. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  93. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  94. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  95. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  96. package/docs/api/interfaces/PaletteData.md +1 -1
  97. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  98. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  99. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  100. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  101. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  102. package/docs/api/interfaces/PublicPageHeaderProps.md +2 -2
  103. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  104. package/docs/api/interfaces/RBACConfig.md +1 -1
  105. package/docs/api/interfaces/RBACContextType.md +1 -1
  106. package/docs/api/interfaces/RBACLogger.md +1 -1
  107. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  108. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  109. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  110. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  111. package/docs/api/interfaces/RouteConfig.md +1 -1
  112. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  113. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  114. package/docs/api/interfaces/StorageConfig.md +1 -1
  115. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  116. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  117. package/docs/api/interfaces/StorageListOptions.md +1 -1
  118. package/docs/api/interfaces/StorageListResult.md +1 -1
  119. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  120. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  121. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  122. package/docs/api/interfaces/StyleImport.md +1 -1
  123. package/docs/api/interfaces/SwitchProps.md +1 -1
  124. package/docs/api/interfaces/ToastActionElement.md +1 -1
  125. package/docs/api/interfaces/ToastProps.md +1 -1
  126. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthProviderProps.md +4 -4
  128. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  130. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  132. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  133. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  134. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  135. package/docs/api/interfaces/UserEventAccess.md +1 -1
  136. package/docs/api/interfaces/UserMenuProps.md +1 -1
  137. package/docs/api/interfaces/UserProfile.md +1 -1
  138. package/docs/api/modules.md +39 -14
  139. package/docs/api-reference/providers.md +16 -7
  140. package/docs/architecture/services.md +374 -0
  141. package/docs/best-practices/README.md +1 -1
  142. package/docs/best-practices/testing.md +1 -1
  143. package/docs/breaking-changes.md +182 -0
  144. package/docs/common-patterns.md +445 -0
  145. package/docs/core-concepts/authentication.md +26 -11
  146. package/docs/core-concepts/events.md +2 -0
  147. package/docs/core-concepts/organisations.md +2 -0
  148. package/docs/core-concepts/permissions.md +2 -0
  149. package/docs/{INDEX.md → documentation-index.md} +26 -38
  150. package/docs/faq.md +286 -0
  151. package/docs/{FILE_REFERENCE_SYSTEM.md → file-reference-system.md} +1 -1
  152. package/docs/getting-started/installation-guide.md +284 -0
  153. package/docs/getting-started/quick-start.md +8 -1
  154. package/docs/implementation-guides/app-layout.md +3 -1
  155. package/docs/implementation-guides/data-tables.md +2 -0
  156. package/docs/implementation-guides/dynamic-colors.md +47 -2
  157. package/docs/implementation-guides/event-theming-summary.md +220 -0
  158. package/docs/implementation-guides/forms.md +9 -7
  159. package/docs/implementation-guides/navigation.md +2 -0
  160. package/docs/migration/service-architecture.md +351 -0
  161. package/docs/migration-guides/unified-auth-provider-mandatory-timeouts.md +226 -0
  162. package/docs/rbac/README-rbac-rls-integration.md +2 -2
  163. package/docs/rbac/README.md +1 -1
  164. package/docs/rbac/examples/rbac-rls-integration-example.md +3 -3
  165. package/docs/rbac/quick-start.md +2 -0
  166. package/docs/rbac/rbac-rls-integration.md +2 -2
  167. package/docs/security/README.md +5 -1
  168. package/docs/style-guide.md +136 -1
  169. package/docs/testing/README.md +1 -1
  170. package/docs/troubleshooting/authentication-issues.md +334 -0
  171. package/docs/troubleshooting/common-issues.md +2 -0
  172. package/docs/troubleshooting/styling-issues.md +199 -144
  173. package/docs/usage.md +23 -2
  174. package/package.json +1 -1
  175. package/src/__tests__/{TESTING_GUIDELINES.md → TEST_GUIDE_CURSOR.md} +20 -0
  176. package/src/__tests__/TEST_GUIDE_HUMAN.md +103 -0
  177. package/src/__tests__/fixtures/test-data.ts +90 -0
  178. package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +260 -0
  179. package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +224 -0
  180. package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +273 -0
  181. package/src/__tests__/helpers/__tests__/test-providers.test.tsx +98 -0
  182. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +436 -0
  183. package/src/__tests__/helpers/__tests__/timer-utils.test.ts +371 -0
  184. package/src/__tests__/helpers/component-test-utils.tsx +14 -4
  185. package/src/__tests__/helpers/optimized-test-setup.ts +68 -0
  186. package/src/__tests__/helpers/test-providers.tsx +329 -0
  187. package/src/__tests__/helpers/test-utils.tsx +91 -45
  188. package/src/__tests__/helpers/timer-utils.ts +71 -0
  189. package/src/__tests__/hooks/usePermissions.test.ts +1 -5
  190. package/src/__tests__/integration/UserProfile.test.tsx +1 -5
  191. package/src/__tests__/rbac/PagePermissionGuard.test.tsx +42 -12
  192. package/src/__tests__/setup.ts +34 -28
  193. package/src/components/Alert/Alert.test.tsx +1 -5
  194. package/src/components/Avatar/Avatar.test.tsx +1 -5
  195. package/src/components/Button/Button.test.tsx +4 -20
  196. package/src/components/Card/Card.test.tsx +1 -5
  197. package/src/components/Checkbox/Checkbox.test.tsx +1 -5
  198. package/src/components/DataTable/__tests__/DataTable.comprehensive.test.tsx +1 -5
  199. package/src/components/DataTable/__tests__/DataTable.test.tsx +45 -49
  200. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +1 -5
  201. package/src/components/DataTable/__tests__/styles.test.ts +382 -0
  202. package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +409 -0
  203. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +634 -0
  204. package/src/components/DataTable/core/__tests__/DataManager.test.ts +519 -0
  205. package/src/components/DataTable/core/__tests__/StateManager.test.ts +714 -0
  206. package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +592 -0
  207. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +354 -0
  208. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +539 -0
  209. package/src/components/Dialog/examples/__tests__/SmartDialogExample.unit.test.tsx +1 -5
  210. package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +1 -8
  211. package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +34 -38
  212. package/src/components/Footer/Footer.test.tsx +1 -5
  213. package/src/components/Form/Form.test.tsx +22 -35
  214. package/src/components/Header/Header.test.tsx +1 -9
  215. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +1 -5
  216. package/src/components/Input/Input.test.tsx +2 -10
  217. package/src/components/LoginForm/LoginForm.test.tsx +1 -5
  218. package/src/components/NavigationMenu/NavigationMenu.test.tsx +24 -24
  219. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +1 -6
  220. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +6 -16
  221. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +1 -5
  222. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.rbac.test.tsx +1 -5
  223. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +1 -7
  224. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +1 -9
  225. package/src/components/PasswordReset/PasswordResetForm.test.tsx +1 -9
  226. package/src/components/PublicLayout/PublicErrorBoundary.tsx +4 -5
  227. package/src/components/PublicLayout/PublicPageHeader.tsx +13 -9
  228. package/src/components/PublicLayout/__tests__/EventLogo.test.tsx +666 -0
  229. package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +457 -0
  230. package/src/components/PublicLayout/__tests__/PublicLoadingSpinner.test.tsx +393 -0
  231. package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +351 -0
  232. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +374 -0
  233. package/src/components/PublicLayout/__tests__/PublicPageLayout.test.tsx +388 -0
  234. package/src/components/Select/Select.bug-test.tsx +69 -0
  235. package/src/components/Select/Select.refactored.tsx +497 -0
  236. package/src/components/Select/Select.test.tsx +42 -49
  237. package/src/components/Select/Select.tsx +5 -2
  238. package/src/components/Select/hooks.ts +254 -0
  239. package/src/components/Switch/Switch.test.tsx +1 -5
  240. package/src/components/Table/__tests__/Table.test.tsx +775 -0
  241. package/src/components/Toast/Toast.test.tsx +15 -8
  242. package/src/components/Tooltip/Tooltip.test.tsx +1 -5
  243. package/src/components/UserMenu/UserMenu.test.tsx +3 -15
  244. package/src/components/__tests__/FileDisplay.test.tsx +575 -0
  245. package/src/components/__tests__/FileUpload.test.tsx +446 -0
  246. package/src/components/__tests__/SuperAdminGuard.test.tsx +422 -354
  247. package/src/hooks/__tests__/ServiceHooks.test.tsx +613 -0
  248. package/src/hooks/__tests__/hooks.integration.test.tsx +1 -10
  249. package/src/hooks/__tests__/useApiFetch.unit.test.ts +10 -14
  250. package/src/hooks/__tests__/useAppConfig.unit.test.ts +307 -0
  251. package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +1 -6
  252. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +1 -5
  253. package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +6 -9
  254. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +321 -0
  255. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +583 -0
  256. package/src/hooks/__tests__/usePublicEventLogo.unit.test.ts +640 -0
  257. package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +435 -0
  258. package/src/hooks/__tests__/useRBAC.unit.test.ts +10 -10
  259. package/src/hooks/__tests__/useStorage.unit.test.ts +751 -0
  260. package/src/hooks/index.ts +3 -0
  261. package/src/hooks/public/usePublicEvent.ts +181 -13
  262. package/src/hooks/public/usePublicRouteParams.ts +13 -3
  263. package/src/hooks/services/useAuth.ts +50 -0
  264. package/src/hooks/services/useAuthService.ts +30 -0
  265. package/src/hooks/services/useCurrentEvent.ts +36 -0
  266. package/src/hooks/services/useCurrentOrganisation.ts +52 -0
  267. package/src/hooks/services/useEventService.ts +30 -0
  268. package/src/hooks/services/useInactivityService.ts +30 -0
  269. package/src/hooks/services/useOrganisationService.ts +30 -0
  270. package/src/hooks/services/usePermissions.ts +70 -0
  271. package/src/hooks/services/useRBACService.ts +30 -0
  272. package/src/hooks/useCounter.test.ts +1 -5
  273. package/src/hooks/useEventTheme.ts +86 -0
  274. package/src/hooks/useOrganisationPermissions.test.ts +2 -5
  275. package/src/hooks/useOrganisationSecurity.test.ts +1 -5
  276. package/src/hooks/usePermissionCache.test.ts +1 -5
  277. package/src/hooks/usePermissionCheck.ts +150 -0
  278. package/src/hooks/useSecureDataAccess.test.ts +1 -5
  279. package/src/index.ts +1 -0
  280. package/src/providers/OrganisationProvider.test.tsx +1 -5
  281. package/src/providers/OrganisationProvider.tsx +56 -4
  282. package/src/providers/UnifiedAuthProvider.test.simple.tsx +42 -6
  283. package/src/providers/UnifiedAuthProvider.test.tsx +1 -5
  284. package/src/providers/UnifiedAuthProvider.tsx +4 -4
  285. package/src/providers/__tests__/AuthProvider.test.tsx +105 -439
  286. package/src/providers/__tests__/AuthProvider.test.tsx.backup +771 -0
  287. package/src/providers/__tests__/EventProvider.test.tsx +211 -110
  288. package/src/providers/__tests__/EventProvider.test.tsx.backup +824 -0
  289. package/src/providers/__tests__/InactivityProvider.test.tsx +1 -5
  290. package/src/providers/__tests__/OrganisationProvider.test.tsx +97 -261
  291. package/src/providers/__tests__/OrganisationProvider.test.tsx.backup +820 -0
  292. package/src/providers/__tests__/ServiceProviders.test.tsx +477 -0
  293. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +72 -504
  294. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup +911 -0
  295. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup2 +166 -0
  296. package/src/providers/services/AuthServiceProvider.tsx +65 -0
  297. package/src/providers/services/EventServiceProvider.tsx +83 -0
  298. package/src/providers/services/InactivityServiceProvider.tsx +83 -0
  299. package/src/providers/services/OrganisationServiceProvider.tsx +77 -0
  300. package/src/providers/services/RBACServiceProvider.tsx +79 -0
  301. package/src/providers/services/UnifiedAuthProvider.tsx +368 -0
  302. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +210 -0
  303. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +269 -0
  304. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +892 -0
  305. package/src/rbac/__tests__/engine.comprehensive.test.ts +954 -0
  306. package/src/rbac/__tests__/integration.authflow.test.tsx +1 -5
  307. package/src/rbac/__tests__/integration.navigation.test.tsx +1 -4
  308. package/src/rbac/__tests__/rbac-core.test.tsx +2 -7
  309. package/src/rbac/__tests__/rbac-functions.test.ts +1 -9
  310. package/src/rbac/__tests__/rbac-integration.test.ts +1 -9
  311. package/src/rbac/api.test.ts +1 -9
  312. package/src/rbac/cache.test.ts +10 -8
  313. package/src/rbac/cli/__tests__/policy-manager.test.ts +339 -0
  314. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -5
  315. package/src/rbac/components/NavigationProvider.test.tsx +1 -5
  316. package/src/rbac/components/PagePermissionProvider.test.tsx +1 -5
  317. package/src/rbac/components/SecureDataProvider.test.tsx +1 -5
  318. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +25 -29
  319. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +27 -30
  320. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +23 -27
  321. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +18 -22
  322. package/src/rbac/config.test.ts +1 -5
  323. package/src/rbac/hooks/useCan.test.ts +262 -9
  324. package/src/rbac/hooks/usePermissions.test.ts +246 -6
  325. package/src/rbac/hooks/useRBAC.simple.test.ts +1 -5
  326. package/src/rbac/hooks/useRBAC.test.ts +472 -198
  327. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +1 -9
  328. package/src/services/AuthService.ts +416 -0
  329. package/src/services/EventService.ts +366 -0
  330. package/src/services/InactivityService.ts +388 -0
  331. package/src/services/OrganisationService.ts +592 -0
  332. package/src/services/RBACService.ts +522 -0
  333. package/src/services/__tests__/AuthService.test.ts +356 -0
  334. package/src/services/__tests__/BaseService.test.ts +314 -0
  335. package/src/services/__tests__/EventService.test.ts +489 -0
  336. package/src/services/__tests__/InactivityService.test.ts +403 -0
  337. package/src/services/__tests__/OrganisationService.test.ts +660 -0
  338. package/src/services/__tests__/RBACService.test.ts +492 -0
  339. package/src/services/base/BaseService.ts +87 -0
  340. package/src/services/interfaces/IAuthService.ts +39 -0
  341. package/src/services/interfaces/IEventService.ts +30 -0
  342. package/src/services/interfaces/IInactivityService.ts +31 -0
  343. package/src/services/interfaces/IOrganisationService.ts +41 -0
  344. package/src/services/interfaces/IRBACService.ts +62 -0
  345. package/src/theming/__tests__/runtime.test.ts +540 -0
  346. package/src/types/__tests__/file-reference.test.ts +447 -0
  347. package/src/types/__tests__/organisation.test.ts +1133 -0
  348. package/src/types/__tests__/theme.test.ts +830 -0
  349. package/src/types/__tests__/type-validation.test.ts +527 -0
  350. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +1 -5
  351. package/src/utils/__tests__/debugLogger.test.ts +417 -0
  352. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +1 -6
  353. package/src/utils/__tests__/dynamicUtils.unit.test.ts +1 -5
  354. package/src/utils/__tests__/lazyLoad.unit.test.tsx +35 -35
  355. package/src/utils/__tests__/organisationContext.unit.test.ts +1 -5
  356. package/src/utils/__tests__/performanceBudgets.unit.test.ts +5 -11
  357. package/src/utils/__tests__/secureErrors.unit.test.ts +1 -6
  358. package/src/utils/__tests__/secureStorage.unit.test.ts +1 -5
  359. package/src/utils/__tests__/securityMonitor.unit.test.ts +1 -5
  360. package/src/utils/__tests__/sessionTracking.unit.test.ts +1 -5
  361. package/src/utils/appIdResolver.test.ts +6 -10
  362. package/src/utils/appNameResolver.simple.test.ts +142 -0
  363. package/src/utils/appNameResolver.test.ts +31 -458
  364. package/src/utils/appNameResolver.test.ts.backup +494 -0
  365. package/src/utils/debugLogger.ts +26 -5
  366. package/src/utils/formatDate.test.ts +1 -5
  367. package/src/utils/organisationContext.test.ts +1 -5
  368. package/src/utils/performanceBudgets.ts +3 -4
  369. package/src/utils/secureDataAccess.test.ts +1 -5
  370. package/src/utils/storage/__tests__/helpers.unit.test.ts +1 -5
  371. package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +1 -5
  372. package/dist/chunk-4HQ5BOVZ.js.map +0 -1
  373. package/dist/chunk-D7ARGIA3.js.map +0 -1
  374. package/dist/chunk-QVEOQVD4.js.map +0 -1
  375. package/dist/chunk-T2MQY57J.js.map +0 -1
  376. package/dist/chunk-T6HVDA24.js.map +0 -1
  377. package/dist/chunk-VTJ5HCZB.js +0 -315
  378. package/dist/chunk-VTJ5HCZB.js.map +0 -1
  379. package/dist/chunk-ZPK5656W.js.map +0 -1
  380. package/docs/getting-started/installation.md +0 -269
  381. package/src/__tests__/REBUILD_PLAN.md +0 -223
  382. /package/dist/{DataTable-MFUXNGPR.js.map → DataTable-MPBSXUC6.js.map} +0 -0
  383. /package/dist/{chunk-CKNY7HYS.js.map → chunk-2ARQW6VX.js.map} +0 -0
  384. /package/dist/{chunk-BTCA3ENN.js.map → chunk-Q2UP3ZWQ.js.map} +0 -0
@@ -0,0 +1,592 @@
1
+ /**
2
+ * @file Organisation Service
3
+ * @package @jmruthers/pace-core
4
+ * @module Services
5
+ * @since 0.1.0
6
+ *
7
+ * Organisation service implementation.
8
+ * Handles organisation management and selection with security-first approach.
9
+ */
10
+
11
+ import { type SupabaseClient, type User, type Session } from '@supabase/supabase-js';
12
+ import { BaseService } from './base/BaseService';
13
+ import { IOrganisationService } from './interfaces/IOrganisationService';
14
+ import type {
15
+ Organisation,
16
+ OrganisationMembership,
17
+ OrganisationSecurityError,
18
+ OrganisationHierarchy
19
+ } from '../types/organisation';
20
+ import { setOrganisationContext } from '../utils/organisationContext';
21
+ import { DebugLogger } from '../utils/debugLogger';
22
+
23
+ export class OrganisationService extends BaseService implements IOrganisationService {
24
+ private _selectedOrganisation: Organisation | null = null;
25
+ private _organisations: Organisation[] = [];
26
+ private _userMemberships: OrganisationMembership[] = [];
27
+ private _roleMapState: Map<string, string> = new Map();
28
+ private _isLoading = true;
29
+ private _error: Error | null = null;
30
+ private _isContextReady = false;
31
+ private retryCount = 0;
32
+
33
+ // Dependencies
34
+ private supabaseClient: SupabaseClient | null = null;
35
+ private user: User | null = null;
36
+ private session: Session | null = null;
37
+
38
+ // Internal state management
39
+ private isLoadingRef = false;
40
+ private lastLoadTimeRef = 0;
41
+ private hasFailedRef = false;
42
+ private abortControllerRef: AbortController | null = null;
43
+
44
+ constructor(supabaseClient: SupabaseClient, user: User | null, session: Session | null) {
45
+ super();
46
+ this.supabaseClient = supabaseClient;
47
+ this.user = user;
48
+ this.session = session;
49
+ }
50
+
51
+ // Interface implementation
52
+ getSelectedOrganisation(): Organisation | null { return this._selectedOrganisation; }
53
+ getOrganisations(): Organisation[] { return this._organisations; }
54
+ getUserMemberships(): OrganisationMembership[] { return this._userMemberships; }
55
+ isLoading(): boolean { return this._isLoading; }
56
+ getError(): Error | null { return this._error; }
57
+ hasValidOrganisationContext(): boolean { return !!(this._selectedOrganisation && !this._isLoading && !this._error && this._isContextReady); }
58
+ isContextReady(): boolean { return this._isContextReady; }
59
+
60
+ // Additional methods for testing
61
+ setSelectedOrganisation(organisation: Organisation | null): void {
62
+ this._selectedOrganisation = organisation;
63
+ if (organisation) {
64
+ localStorage.setItem('pace-core-selected-organisation', JSON.stringify(organisation));
65
+ this.setDatabaseOrganisationContext(organisation);
66
+ } else {
67
+ localStorage.removeItem('pace-core-selected-organisation');
68
+ this._isContextReady = false;
69
+ }
70
+ this.notify();
71
+ }
72
+
73
+ // For testing: expose dependencies
74
+ getDependencies(): { user: User | null; session: Session | null; supabaseClient: SupabaseClient | null } {
75
+ return {
76
+ user: this.user,
77
+ session: this.session,
78
+ supabaseClient: this.supabaseClient
79
+ };
80
+ }
81
+
82
+ // For testing: manually set state
83
+ setTestState(
84
+ organisations: Organisation[],
85
+ memberships: OrganisationMembership[],
86
+ roleMap: Map<string, string>,
87
+ selectedOrg: Organisation | null = null
88
+ ): void {
89
+ this._organisations = organisations;
90
+ this._userMemberships = memberships;
91
+ this._roleMapState = roleMap;
92
+ if (selectedOrg) {
93
+ this._selectedOrganisation = selectedOrg;
94
+ } else if (organisations.length > 0) {
95
+ this._selectedOrganisation = organisations[0];
96
+ }
97
+ this._isLoading = false;
98
+ this._error = null;
99
+ this.notify();
100
+ }
101
+
102
+ // Update dependencies
103
+ updateDependencies(user: User | null, session: Session | null): void {
104
+ this.user = user;
105
+ this.session = session;
106
+ this.notify();
107
+ }
108
+
109
+ // Organisation methods
110
+ async switchOrganisation(orgId: string): Promise<void> {
111
+ DebugLogger.log("OrganisationService", "Switching to organisation:", orgId);
112
+
113
+ // Validate access
114
+ if (!this.validateOrganisationAccess(orgId)) {
115
+ throw new Error(`User does not have access to organisation ${orgId}`) as OrganisationSecurityError;
116
+ }
117
+
118
+ const targetOrg = this._organisations.find(org => org.id === orgId);
119
+ if (!targetOrg) {
120
+ throw new Error(`Organisation ${orgId} not found in user's organisations`) as OrganisationSecurityError;
121
+ }
122
+
123
+ this._selectedOrganisation = targetOrg;
124
+
125
+ // Persist selection
126
+ localStorage.setItem('pace-core-selected-organisation', JSON.stringify(targetOrg));
127
+
128
+ // Set database organisation context
129
+ await this.setDatabaseOrganisationContext(targetOrg);
130
+
131
+ DebugLogger.log("OrganisationService", "Switched to organisation:", targetOrg.display_name);
132
+ this.notify();
133
+ }
134
+
135
+ getUserRole(orgId?: string): string {
136
+ const targetOrgId = orgId || this._selectedOrganisation?.id;
137
+ if (!targetOrgId) return 'no_access';
138
+
139
+ // Use roleMapState to get the role for this organisation
140
+ return this._roleMapState.get(targetOrgId) || 'no_access';
141
+ }
142
+
143
+ validateOrganisationAccess(orgId: string): boolean {
144
+ return this._userMemberships.some((m: any) =>
145
+ m.organisation_id === orgId &&
146
+ m.status === 'active' &&
147
+ m.revoked_at === null
148
+ );
149
+ }
150
+
151
+ async refreshOrganisations(): Promise<void> {
152
+ if (!this.user || !this.session || !this.supabaseClient) return;
153
+
154
+ // Force reload by triggering the effect
155
+ this._isLoading = true;
156
+ this.notify();
157
+ await this.loadUserOrganisations();
158
+ }
159
+
160
+ ensureOrganisationContext(): Organisation {
161
+ if (!this._selectedOrganisation) {
162
+ throw new Error('Organisation context is required but not available') as OrganisationSecurityError;
163
+ }
164
+ return this._selectedOrganisation;
165
+ }
166
+
167
+ isOrganisationSecure(): boolean {
168
+ return !!(this._selectedOrganisation && this.user);
169
+ }
170
+
171
+ getPrimaryOrganisation(): Organisation | null {
172
+ // Look for org_admin role first, then leader, then member
173
+ const rolePriority = ['org_admin', 'leader', 'member'];
174
+
175
+ for (const role of rolePriority) {
176
+ const membership = this._userMemberships.find((m: any) => m.role === role);
177
+ if (membership) {
178
+ return this._organisations.find((org: any) => org.id === membership.organisation_id) || null;
179
+ }
180
+ }
181
+
182
+ return null;
183
+ }
184
+
185
+ buildOrganisationHierarchy(orgs: Organisation[]): OrganisationHierarchy[] {
186
+ const orgMap = new Map<string, Organisation>();
187
+ orgs.forEach(org => orgMap.set(org.id, org));
188
+
189
+ const roots: OrganisationHierarchy[] = [];
190
+
191
+ orgs.forEach(org => {
192
+ if (!org.parent_id) {
193
+ // Root organisation
194
+ roots.push({
195
+ organisation: org,
196
+ children: [],
197
+ depth: 0
198
+ });
199
+ }
200
+ });
201
+
202
+ // For now, return flat structure - hierarchy building can be added later
203
+ return roots;
204
+ }
205
+
206
+ // Lifecycle methods
207
+ async initialize(): Promise<void> {
208
+ await super.initialize();
209
+ await this.loadUserOrganisations();
210
+ }
211
+
212
+ cleanup(): void {
213
+ // Cleanup on unmount
214
+ this.isLoadingRef = false;
215
+ this.hasFailedRef = false;
216
+ this.lastLoadTimeRef = 0;
217
+ // Abort any pending requests
218
+ if (this.abortControllerRef) {
219
+ this.abortControllerRef.abort();
220
+ this.abortControllerRef = null;
221
+ }
222
+ // Reset state
223
+ this._selectedOrganisation = null;
224
+ this._organisations = [];
225
+ this._userMemberships = [];
226
+ this._roleMapState = new Map();
227
+ this._isLoading = false;
228
+ this._error = null;
229
+ this._isContextReady = false;
230
+ super.cleanup();
231
+ }
232
+
233
+ protected async doInitialize(): Promise<void> {
234
+ // Initial setup
235
+ }
236
+
237
+ protected doCleanup(): void {
238
+ // Cleanup any resources
239
+ }
240
+
241
+ private async setDatabaseOrganisationContext(organisation: Organisation): Promise<void> {
242
+ if (!this.supabaseClient || !this.session) {
243
+ console.warn('[OrganisationService] No Supabase client or session available for setting organisation context');
244
+ this._isContextReady = false;
245
+ this.notify();
246
+ return;
247
+ }
248
+
249
+ try {
250
+ await setOrganisationContext(this.supabaseClient, organisation.id);
251
+ DebugLogger.log('OrganisationService', 'Database organisation context set to:', organisation.display_name);
252
+ this._isContextReady = true;
253
+ this.notify();
254
+ } catch (error) {
255
+ console.error('[OrganisationService] Failed to set database organisation context:', error);
256
+ this._isContextReady = false;
257
+ this.notify();
258
+ // Don't throw - this is a non-critical operation
259
+ }
260
+ }
261
+
262
+ private async loadUserOrganisations(): Promise<void> {
263
+ // Add call tracking to detect race conditions
264
+ const callId = Math.random().toString(36).substr(2, 9);
265
+ console.log(`[OrganisationService] Starting loadUserOrganisations call ${callId}`);
266
+
267
+ if (!this.user || !this.session || !this.supabaseClient) {
268
+ // Clear state when no user, session, or supabase client
269
+ DebugLogger.log('OrganisationService', 'Clearing organisation state - no user, session, or supabase client');
270
+ this._selectedOrganisation = null;
271
+ this._organisations = [];
272
+ this._userMemberships = [];
273
+ this._isLoading = false;
274
+ this._error = null;
275
+ this.notify();
276
+ return;
277
+ }
278
+
279
+ // Additional check to prevent loading during auth state changes
280
+ if (this.isLoadingRef) {
281
+ console.log("OrganisationService", "Already loading, skipping duplicate load");
282
+ return;
283
+ }
284
+
285
+ // Prevent rapid retries - minimum 2 seconds between attempts
286
+ const now = Date.now();
287
+ if (now - this.lastLoadTimeRef < 2000) {
288
+ console.log("OrganisationService", "Too soon since last load, skipping");
289
+ return;
290
+ }
291
+
292
+ // Cancel any existing request
293
+ if (this.abortControllerRef) {
294
+ this.abortControllerRef.abort();
295
+ }
296
+
297
+ // Create new abort controller for this request
298
+ this.abortControllerRef = new AbortController();
299
+ const abortSignal = this.abortControllerRef.signal;
300
+
301
+ this.lastLoadTimeRef = now;
302
+ this.isLoadingRef = true;
303
+ this._isLoading = true;
304
+ this._error = null;
305
+ this.notify();
306
+
307
+ try {
308
+ DebugLogger.log("OrganisationService", "Loading organisations for user:", this.user.id);
309
+
310
+ // Debug: Log Supabase client configuration
311
+ console.log("[OrganisationService] Supabase client ready:", {
312
+ isConnected: !!this.supabaseClient,
313
+ hasAuth: !!this.supabaseClient.auth,
314
+ hasRpc: !!this.supabaseClient.rpc
315
+ });
316
+
317
+ // Get user's organisation memberships using secure RPC function
318
+ // Only get actual members (org_admin, leader, member) - exclude supporters
319
+ let memberships, membershipError;
320
+ try {
321
+ console.log("[OrganisationService] Making RPC call to data_user_organisation_roles_get...");
322
+
323
+ // Add timeout and abort signal to prevent hanging RPC calls
324
+ const timeoutPromise = new Promise((_, reject) => {
325
+ const timeoutId = setTimeout(() => reject(new Error('RPC call timeout after 10 seconds')), 10000);
326
+ abortSignal.addEventListener('abort', () => {
327
+ clearTimeout(timeoutId);
328
+ reject(new Error('Request aborted'));
329
+ });
330
+ });
331
+
332
+ const rpcPromise = this.supabaseClient.rpc('data_user_organisation_roles_get', {
333
+ p_user_id: this.user.id,
334
+ p_organisation_id: null
335
+ });
336
+
337
+ // Check if request was aborted before making the call
338
+ if (abortSignal.aborted) {
339
+ throw new Error('Request aborted');
340
+ }
341
+
342
+ const result = await Promise.race([rpcPromise, timeoutPromise]) as any;
343
+
344
+ console.log("[OrganisationService] RPC call completed:", {
345
+ hasData: !!result.data,
346
+ hasError: !!result.error,
347
+ dataLength: result.data?.length || 0,
348
+ errorMessage: result.error?.message || 'No error'
349
+ });
350
+
351
+ // Filter to only actual members (org_admin, leader, member) - exclude supporters
352
+ memberships = result.data?.filter((role: any) =>
353
+ ['org_admin', 'leader', 'member'].includes(role.role)
354
+ ) || [];
355
+ membershipError = result.error;
356
+ } catch (queryError: any) {
357
+ membershipError = queryError;
358
+ }
359
+
360
+ if (membershipError) {
361
+ console.error("[OrganisationService] Error loading memberships:", membershipError);
362
+
363
+ // If RPC fails with timeout, try direct database query as fallback
364
+ if (membershipError.message?.includes('timeout')) {
365
+ console.log("[OrganisationService] RPC timed out, trying direct database query as fallback...");
366
+ try {
367
+ // Check if request was aborted before making fallback query
368
+ if (abortSignal.aborted) {
369
+ throw new Error('Request aborted');
370
+ }
371
+
372
+ const { data: fallbackData, error: fallbackError } = await this.supabaseClient
373
+ .from('rbac_organisation_roles')
374
+ .select(`
375
+ id,
376
+ user_id,
377
+ organisation_id,
378
+ role,
379
+ status,
380
+ granted_at,
381
+ granted_by,
382
+ revoked_at,
383
+ revoked_by,
384
+ notes,
385
+ created_at,
386
+ updated_at,
387
+ organisations!inner(
388
+ id,
389
+ name,
390
+ display_name,
391
+ subscription_tier,
392
+ settings,
393
+ is_active,
394
+ parent_id,
395
+ created_at,
396
+ updated_at
397
+ )
398
+ `)
399
+ .eq('user_id', this.user.id)
400
+ .eq('status', 'active')
401
+ .is('revoked_at', null)
402
+ .in('role', ['org_admin', 'leader', 'member']);
403
+
404
+ if (fallbackError) {
405
+ console.error("[OrganisationService] Fallback query also failed:", fallbackError);
406
+ throw membershipError; // Throw original error
407
+ }
408
+
409
+ console.log("[OrganisationService] Fallback query successful, got", fallbackData?.length || 0, "memberships");
410
+ memberships = fallbackData || [];
411
+ membershipError = null;
412
+ } catch (fallbackErr) {
413
+ console.error("[OrganisationService] Fallback query failed:", fallbackErr);
414
+ throw membershipError; // Throw original error
415
+ }
416
+ } else {
417
+ throw membershipError;
418
+ }
419
+ }
420
+
421
+ DebugLogger.log("OrganisationService", "Raw memberships data:", memberships);
422
+
423
+ if (!memberships || memberships.length === 0) {
424
+ throw new Error('User has no active organisation memberships') as OrganisationSecurityError;
425
+ }
426
+
427
+ // Get organisation details for the memberships
428
+ const organisationIds = memberships
429
+ .map((m: any) => m.organisation_id)
430
+ .filter((id: string) => {
431
+ // Better validation to prevent empty string UUID errors
432
+ if (!id || typeof id !== 'string') {
433
+ console.warn("[OrganisationService] Invalid organisation ID (not string):", id);
434
+ return false;
435
+ }
436
+ const trimmedId = id.trim();
437
+ if (trimmedId === '') {
438
+ console.warn("[OrganisationService] Empty organisation ID found");
439
+ return false;
440
+ }
441
+ // Validate UUID format
442
+ const isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmedId);
443
+ if (!isValidUuid) {
444
+ console.warn("[OrganisationService] Invalid UUID format:", trimmedId);
445
+ }
446
+ return isValidUuid;
447
+ });
448
+
449
+ if (organisationIds.length === 0) {
450
+ console.warn("[OrganisationService] No valid organisation IDs found in memberships:", memberships);
451
+ throw new Error('No valid organisation IDs found in memberships') as OrganisationSecurityError;
452
+ }
453
+
454
+ DebugLogger.log("OrganisationService", "Valid organisation IDs:", organisationIds);
455
+
456
+ // Check if request was aborted before making organisations query
457
+ if (abortSignal.aborted) {
458
+ throw new Error('Request aborted');
459
+ }
460
+
461
+ const { data: allOrganisations, error: orgError } = await this.supabaseClient
462
+ .from('organisations')
463
+ .select('id, name, display_name, subscription_tier, settings, is_active, parent_id, created_at, updated_at');
464
+
465
+ if (orgError) {
466
+ console.error("[OrganisationService] Error loading organisations:", orgError);
467
+ throw orgError;
468
+ }
469
+
470
+ // Filter manually on the client side
471
+ const organisations = allOrganisations?.filter(org =>
472
+ organisationIds.includes(org.id)
473
+ ) || [];
474
+
475
+ // Create a map of organisation_id to role from the memberships data
476
+ const roleMap = new Map<string, string>();
477
+ memberships?.forEach((membership: any) => {
478
+ roleMap.set(membership.organisation_id, membership.role);
479
+ });
480
+
481
+ // Extract organisations and memberships
482
+ const orgs = organisations as Organisation[];
483
+ const activeOrgs = orgs.filter(org => org.is_active);
484
+
485
+ if (activeOrgs.length === 0) {
486
+ throw new Error('User has no access to active organisations') as OrganisationSecurityError;
487
+ }
488
+
489
+ DebugLogger.log("OrganisationService", "Active organisations:", activeOrgs);
490
+
491
+ this._organisations = activeOrgs;
492
+ this._userMemberships = memberships as OrganisationMembership[];
493
+
494
+ // Store role map in component state for later use
495
+ this._roleMapState = roleMap;
496
+
497
+ // Auto-select organisation: try persisted, then primary, then first
498
+ let initialOrg: Organisation | null = null;
499
+
500
+ // 1. Try to restore from localStorage
501
+ try {
502
+ const persistedOrgString = localStorage.getItem('pace-core-selected-organisation');
503
+ if (persistedOrgString) {
504
+ const persistedOrg = JSON.parse(persistedOrgString) as Organisation;
505
+ // Validate persisted org ID before using it
506
+ if (persistedOrg.id && typeof persistedOrg.id === 'string' && persistedOrg.id.trim() !== '') {
507
+ const validPersistedOrg = activeOrgs.find(org => org.id === persistedOrg.id);
508
+ if (validPersistedOrg) {
509
+ initialOrg = validPersistedOrg;
510
+ DebugLogger.log("OrganisationService", "Restored persisted organisation:", initialOrg.display_name);
511
+ } else {
512
+ console.warn("[OrganisationService] Persisted organisation not found in active orgs, clearing cache");
513
+ localStorage.removeItem('pace-core-selected-organisation');
514
+ }
515
+ } else {
516
+ console.warn("[OrganisationService] Invalid persisted organisation ID, clearing cache");
517
+ localStorage.removeItem('pace-core-selected-organisation');
518
+ }
519
+ }
520
+ } catch (storageError) {
521
+ console.warn("[OrganisationService] Failed to restore persisted organisation:", storageError);
522
+ // Clear potentially corrupted cache
523
+ localStorage.removeItem('pace-core-selected-organisation');
524
+ }
525
+
526
+ // 2. Fall back to org_admin role organisation (highest privilege)
527
+ if (!initialOrg) {
528
+ const adminMembership = memberships.find((m: any) => m.role === 'org_admin');
529
+ if (adminMembership) {
530
+ const foundOrg = organisations.find((org: any) => org.id === adminMembership.organisation_id);
531
+ if (foundOrg) {
532
+ initialOrg = foundOrg;
533
+ DebugLogger.log("OrganisationService", "Selected org_admin organisation:", initialOrg.display_name);
534
+ }
535
+ }
536
+ }
537
+
538
+ // 3. Fall back to first organisation
539
+ if (!initialOrg) {
540
+ initialOrg = activeOrgs[0];
541
+ DebugLogger.log("OrganisationService", "Selected first organisation:", initialOrg.display_name);
542
+ }
543
+
544
+ if (!initialOrg) {
545
+ throw new Error('No valid organisation found for user') as OrganisationSecurityError;
546
+ }
547
+
548
+ this._selectedOrganisation = initialOrg;
549
+
550
+ // Persist selection
551
+ localStorage.setItem('pace-core-selected-organisation', JSON.stringify(initialOrg));
552
+
553
+ DebugLogger.log("OrganisationService", "Organisation context established:", {
554
+ selectedOrganisation: initialOrg.display_name,
555
+ totalOrganisations: activeOrgs.length,
556
+ userRole: roleMap.get(initialOrg.id)
557
+ });
558
+
559
+ // Reset retry count and failed flag on success
560
+ this.retryCount = 0;
561
+ this.hasFailedRef = false;
562
+
563
+ } catch (err) {
564
+ console.error("[OrganisationService] Failed to load organisations:", err);
565
+ this._error = err as Error;
566
+ // Increment retry count on error
567
+ this.retryCount = this.retryCount + 1;
568
+ // Set failed flag to prevent further attempts
569
+ this.hasFailedRef = true;
570
+ // Clear all cached data on error to prevent corruption
571
+ this.clearAllCachedData();
572
+ } finally {
573
+ // Always cleanup refs and abort controller
574
+ this.isLoadingRef = false;
575
+ this._isLoading = false;
576
+ this.abortControllerRef = null;
577
+ this.notify();
578
+ }
579
+ }
580
+
581
+ private clearAllCachedData(): void {
582
+ localStorage.removeItem('pace-core-selected-organisation');
583
+ localStorage.removeItem('pace-core-organisation-context');
584
+ this._selectedOrganisation = null;
585
+ this._organisations = [];
586
+ this._userMemberships = [];
587
+ this._roleMapState = new Map();
588
+ this.retryCount = 0;
589
+ this._isContextReady = false;
590
+ // Don't clear _error here - let it persist for error reporting
591
+ }
592
+ }