@jmruthers/pace-core 0.6.3 → 0.6.5

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 (103) hide show
  1. package/dist/{DataTable-THFPBKTP.js → DataTable-AOVNCPTX.js} +8 -8
  2. package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-QTFVrL-Z.d.ts} +65 -83
  3. package/dist/{UnifiedAuthProvider-KAGUYQ4J.js → UnifiedAuthProvider-4SBX4LU5.js} +4 -4
  4. package/dist/{api-IAGWF3ZG.js → api-O6HTBX5Y.js} +3 -3
  5. package/dist/{chunk-ZNIWI3UC.js → chunk-6COVEUS7.js} +141 -107
  6. package/dist/chunk-6COVEUS7.js.map +1 -0
  7. package/dist/{chunk-QRPVRXYT.js → chunk-AFVQODI2.js} +38 -1
  8. package/dist/{chunk-QRPVRXYT.js.map → chunk-AFVQODI2.js.map} +1 -1
  9. package/dist/{chunk-RWEBCB47.js → chunk-EFN2EIMK.js} +2 -2
  10. package/dist/{chunk-CNCQDFLN.js → chunk-G7QEZTYQ.js} +31 -31
  11. package/dist/{chunk-CNCQDFLN.js.map → chunk-G7QEZTYQ.js.map} +1 -1
  12. package/dist/{chunk-YDQHOZNA.js → chunk-HU2C6SSC.js} +29 -18
  13. package/dist/chunk-HU2C6SSC.js.map +1 -0
  14. package/dist/{chunk-DWUBLJJM.js → chunk-IHB5DR3H.js} +184 -53
  15. package/dist/chunk-IHB5DR3H.js.map +1 -0
  16. package/dist/{chunk-PQBSKX33.js → chunk-IVOFDYWT.js} +364 -208
  17. package/dist/chunk-IVOFDYWT.js.map +1 -0
  18. package/dist/{chunk-6SOIHG6Z.js → chunk-JGRYX5UX.js} +120 -20
  19. package/dist/chunk-JGRYX5UX.js.map +1 -0
  20. package/dist/{chunk-6Z7LTB3D.js → chunk-NTM7ZSB6.js} +4 -4
  21. package/dist/chunk-NTM7ZSB6.js.map +1 -0
  22. package/dist/{chunk-HFZBI76P.js → chunk-RGAWHO7N.js} +4 -4
  23. package/dist/chunk-RGAWHO7N.js.map +1 -0
  24. package/dist/{chunk-2T2IG7T7.js → chunk-UPPMRMYG.js} +3 -3
  25. package/dist/{chunk-2T2IG7T7.js.map → chunk-UPPMRMYG.js.map} +1 -1
  26. package/dist/components.d.ts +2 -3
  27. package/dist/components.js +24 -28
  28. package/dist/components.js.map +1 -1
  29. package/dist/{contextValidator-3JNZKUTX.js → contextValidator-5OGXSPKS.js} +2 -2
  30. package/dist/hooks.d.ts +3 -3
  31. package/dist/hooks.js +41 -139
  32. package/dist/hooks.js.map +1 -1
  33. package/dist/index.d.ts +27 -18
  34. package/dist/index.js +41 -50
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers.js +3 -3
  37. package/dist/rbac/index.d.ts +16 -9
  38. package/dist/rbac/index.js +6 -6
  39. package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-ClnV4tnv.d.ts} +8 -8
  40. package/dist/utils.js +1 -1
  41. package/docs/api/modules.md +210 -100
  42. package/package.json +8 -4
  43. package/scripts/audit/core/checks/dependencies.cjs +9 -0
  44. package/scripts/validate-master.js +1 -1
  45. package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
  46. package/src/components/DataTable/components/ImportModal.tsx +4 -6
  47. package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
  48. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
  49. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
  50. package/src/components/DataTable/core/DataTableContext.tsx +1 -1
  51. package/src/components/DateTimeField/DateTimeField.tsx +17 -19
  52. package/src/components/DateTimeField/README.md +5 -2
  53. package/src/components/Dialog/Dialog.test.tsx +248 -228
  54. package/src/components/Dialog/Dialog.tsx +455 -325
  55. package/src/components/Dialog/index.ts +3 -3
  56. package/src/components/FileDisplay/FileDisplay.test.tsx +41 -0
  57. package/src/components/FileDisplay/FileDisplay.tsx +5 -5
  58. package/src/components/Form/Form.test.tsx +3 -2
  59. package/src/components/Form/Form.tsx +4 -5
  60. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
  61. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
  62. package/src/components/LoginForm/LoginForm.tsx +2 -2
  63. package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
  64. package/src/components/PaceAppLayout/PaceAppLayout.tsx +54 -42
  65. package/src/components/PaceAppLayout/README.md +10 -9
  66. package/src/components/PaceAppLayout/test-setup.tsx +40 -31
  67. package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
  68. package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
  69. package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
  70. package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
  71. package/src/components/UserMenu/UserMenu.test.tsx +38 -6
  72. package/src/components/UserMenu/UserMenu.tsx +36 -34
  73. package/src/components/index.ts +3 -4
  74. package/src/hooks/useEventTheme.ts +4 -4
  75. package/src/hooks/useEvents.ts +11 -7
  76. package/src/hooks/useKeyboardShortcuts.ts +1 -1
  77. package/src/hooks/useOrganisationPermissions.ts +4 -4
  78. package/src/hooks/useOrganisations.ts +13 -7
  79. package/src/index.ts +11 -1
  80. package/src/rbac/README.md +20 -20
  81. package/src/rbac/hooks/useRBAC.test.ts +21 -3
  82. package/src/rbac/hooks/useRBAC.ts +4 -3
  83. package/src/rbac/hooks/useResourcePermissions.test.ts +125 -30
  84. package/src/rbac/hooks/useResourcePermissions.ts +57 -29
  85. package/src/rbac/permissions.ts +17 -17
  86. package/src/rbac/utils/contextValidator.ts +36 -0
  87. package/src/services/AuthService.ts +2 -5
  88. package/src/services/EventService.ts +99 -2
  89. package/src/services/InactivityService.ts +139 -58
  90. package/src/styles/core.css +4 -0
  91. package/src/utils/formatting/formatTime.test.ts +3 -2
  92. package/dist/chunk-6SOIHG6Z.js.map +0 -1
  93. package/dist/chunk-6Z7LTB3D.js.map +0 -1
  94. package/dist/chunk-DWUBLJJM.js.map +0 -1
  95. package/dist/chunk-HFZBI76P.js.map +0 -1
  96. package/dist/chunk-PQBSKX33.js.map +0 -1
  97. package/dist/chunk-YDQHOZNA.js.map +0 -1
  98. package/dist/chunk-ZNIWI3UC.js.map +0 -1
  99. /package/dist/{DataTable-THFPBKTP.js.map → DataTable-AOVNCPTX.js.map} +0 -0
  100. /package/dist/{UnifiedAuthProvider-KAGUYQ4J.js.map → UnifiedAuthProvider-4SBX4LU5.js.map} +0 -0
  101. /package/dist/{api-IAGWF3ZG.js.map → api-O6HTBX5Y.js.map} +0 -0
  102. /package/dist/{chunk-RWEBCB47.js.map → chunk-EFN2EIMK.js.map} +0 -0
  103. /package/dist/{contextValidator-3JNZKUTX.js.map → contextValidator-5OGXSPKS.js.map} +0 -0
@@ -46,7 +46,7 @@ import { useOrganisations } from '../../hooks/useOrganisations';
46
46
  import { useEvents } from '../../hooks/useEvents';
47
47
  import { useResolvedScope } from './useResolvedScope';
48
48
  import { useCan } from './usePermissions';
49
- import type { Scope } from '../types';
49
+ import type { Scope, Permission } from '../types';
50
50
 
51
51
  export interface UseResourcePermissionsOptions {
52
52
  /** Whether to check read permissions (default: false) */
@@ -80,15 +80,22 @@ export interface ResourcePermissions {
80
80
  * and provides a simple API for permission checking.
81
81
  *
82
82
  * **Page Permission Support:**
83
- * When an `appId` is available in the resolved scope, the resource name is passed
84
- * as `pageId` to enable page-based permission checks. This allows the hook to work
85
- * with both resource-based permissions (when appId is not available) and page-based
86
- * permissions (when appId is available and the resource is a registered page).
83
+ * When an `appId` is available in the resolved scope, the hook automatically:
84
+ * 1. Waits for scope resolution to complete (including `appId` being set)
85
+ * 2. Constructs permission strings with the `page.` prefix (e.g., `create:page.planning`)
86
+ * 3. Passes the resource name as `pageId` to enable page-based permission checks
87
87
  *
88
- * The RPC function `rbac_check_permission_simplified` will automatically resolve
89
- * the page name to a page ID and check page permissions if the resource matches
90
- * a registered page in `rbac_app_pages`. If the resource is not a registered page,
91
- * it will fall back to resource-based permission checking.
88
+ * This ensures permission strings match the format returned by `rbac_permissions_get`
89
+ * (e.g., `create:page.planning`) rather than resource-based format (e.g., `create:planning`).
90
+ *
91
+ * **Scope Resolution Timing:**
92
+ * The hook waits for scope resolution to complete before constructing permission strings.
93
+ * This prevents timing issues where permission checks use the wrong format (e.g., `delete:planning`
94
+ * instead of `delete:page.planning`) when `appId` is not yet available in the scope.
95
+ *
96
+ * The RPC function `rbac_check_permission_simplified` will resolve the page name to a page ID
97
+ * and check page permissions if the resource matches a registered page in `rbac_app_pages`.
98
+ * If the resource is not a registered page, it will fall back to resource-based permission checking.
92
99
  *
93
100
  * @param resource - The resource name (e.g., 'contacts', 'risks', 'planning')
94
101
  * Can be a resource name or a page name registered in rbac_app_pages
@@ -157,26 +164,42 @@ export function useResourcePermissions(
157
164
  selectedEventId: selectedEvent?.event_id || null
158
165
  });
159
166
 
160
- // Create fallback scope if resolvedScope is not available
167
+ // CRITICAL FIX: Only use resolvedScope when it's available (not during loading)
168
+ // This ensures we wait for appId to be resolved before constructing permission strings
169
+ // If resolvedScope is null (still loading), we can't determine if we should use page permissions
170
+ // so we must wait for scope resolution to complete
161
171
  const scope: Scope = resolvedScope || {
162
172
  organisationId: selectedOrganisation?.id || '',
163
173
  eventId: selectedEvent?.event_id || undefined,
164
174
  appId: undefined
165
175
  };
166
176
 
167
- // If we have an appId in scope, pass the resource name as pageId to enable page permission checks
168
- // The RPC function rbac_check_permission_simplified will resolve the page name to a page ID
169
- // and check page permissions if the resource is a registered page
170
- // This allows useResourcePermissions to work with both resource-based and page-based permissions
171
- const pageId = scope.appId ? resource : undefined;
177
+ // CRITICAL FIX: Only use page permissions when appId is actually available in resolvedScope
178
+ // If scope is still loading (resolvedScope is null), we can't know if appId will be available
179
+ // so we must wait for scope resolution before constructing page permission strings
180
+ // This prevents using wrong permission format (delete:planning instead of delete:page.planning)
181
+ const hasAppId = !!resolvedScope?.appId;
182
+ const pageId = hasAppId ? resource : undefined;
183
+
184
+ // When appId is available in resolved scope, construct permission strings with page. prefix
185
+ // This matches the format that rbac_permissions_get returns (e.g., 'create:page.planning')
186
+ // and ensures consistent permission checking for page-based resources
187
+ // IMPORTANT: Only use page format when appId is actually resolved, not during loading
188
+ const isPagePermission = hasAppId && !!pageId;
189
+ const createPermission = isPagePermission ? `create:page.${resource}` : `create:${resource}`;
190
+ const updatePermission = isPagePermission ? `update:page.${resource}` : `update:${resource}`;
191
+ const deletePermission = isPagePermission ? `delete:page.${resource}` : `delete:${resource}`;
192
+ const readPermission = isPagePermission ? `read:page.${resource}` : `read:${resource}`;
172
193
 
173
194
  // Permission checks for create, update, delete
174
195
  // Pass null for super admin status (not checked yet - hook will check if needed)
175
196
  // PERFORMANCE: These hooks will each check super admin separately - could be optimized in future
197
+ // CRITICAL: useCan will wait for appId when pageId is provided (it checks needsAppIdForPageName)
198
+ // But we must ensure permission strings are correct before calling useCan
176
199
  const { can: canCreateResult, isLoading: createLoading, error: createError } = useCan(
177
200
  user?.id || '',
178
201
  scope,
179
- `create:${resource}` as const,
202
+ createPermission as Permission,
180
203
  pageId, // Pass resource name as pageId when appId is available to enable page permission checks
181
204
  true, // useCache
182
205
  null, // precomputedSuperAdmin - not checked yet
@@ -186,7 +209,7 @@ export function useResourcePermissions(
186
209
  const { can: canUpdateResult, isLoading: updateLoading, error: updateError } = useCan(
187
210
  user?.id || '',
188
211
  scope,
189
- `update:${resource}` as const,
212
+ updatePermission as Permission,
190
213
  pageId, // Pass resource name as pageId when appId is available to enable page permission checks
191
214
  true, // useCache
192
215
  null, // precomputedSuperAdmin - not checked yet
@@ -196,7 +219,7 @@ export function useResourcePermissions(
196
219
  const { can: canDeleteResult, isLoading: deleteLoading, error: deleteError } = useCan(
197
220
  user?.id || '',
198
221
  scope,
199
- `delete:${resource}` as const,
222
+ deletePermission as Permission,
200
223
  pageId, // Pass resource name as pageId when appId is available to enable page permission checks
201
224
  true, // useCache
202
225
  null, // precomputedSuperAdmin - not checked yet
@@ -207,7 +230,7 @@ export function useResourcePermissions(
207
230
  const { can: canReadResult, isLoading: readLoading, error: readError } = useCan(
208
231
  user?.id || '',
209
232
  scope,
210
- `read:${resource}` as const,
233
+ readPermission as Permission,
211
234
  pageId, // Pass resource name as pageId when appId is available to enable page permission checks
212
235
  true, // useCache
213
236
  null, // precomputedSuperAdmin - not checked yet
@@ -215,9 +238,14 @@ export function useResourcePermissions(
215
238
  );
216
239
 
217
240
  // Aggregate loading states - any permission check or scope resolution loading
241
+ // CRITICAL: When requireScope is true, we must wait for scope resolution to complete
242
+ // so we can determine the correct permission format (page vs resource permissions)
243
+ // This prevents using wrong permission format (delete:planning instead of delete:page.planning)
218
244
  const isLoading = useMemo(() => {
219
- return scopeLoading || createLoading || updateLoading || deleteLoading || (enableRead && readLoading);
220
- }, [scopeLoading, createLoading, updateLoading, deleteLoading, readLoading, enableRead]);
245
+ // If scope resolution is required, wait for it to complete
246
+ const waitingForScope = requireScope && scopeLoading;
247
+ return waitingForScope || createLoading || updateLoading || deleteLoading || (enableRead && readLoading);
248
+ }, [scopeLoading, requireScope, createLoading, updateLoading, deleteLoading, readLoading, enableRead]);
221
249
 
222
250
  // Aggregate errors - prefer scope error, then any permission error
223
251
  const error = useMemo(() => {
@@ -239,19 +267,19 @@ export function useResourcePermissions(
239
267
  if (res !== resource) {
240
268
  return false;
241
269
  }
242
- return canCreateResult;
270
+ return canCreateResult; // canCreateResult is already the boolean 'can' value from useCan
243
271
  },
244
272
  canUpdate: (res: string) => {
245
273
  if (res !== resource) {
246
274
  return false;
247
275
  }
248
- return canUpdateResult;
276
+ return canUpdateResult; // canUpdateResult is already the boolean 'can' value from useCan
249
277
  },
250
278
  canDelete: (res: string) => {
251
279
  if (res !== resource) {
252
280
  return false;
253
281
  }
254
- return canDeleteResult;
282
+ return canDeleteResult; // canDeleteResult is already the boolean 'can' value from useCan
255
283
  },
256
284
  canRead: (res: string) => {
257
285
  if (!enableRead) {
@@ -260,17 +288,17 @@ export function useResourcePermissions(
260
288
  if (res !== resource) {
261
289
  return false;
262
290
  }
263
- return canReadResult;
291
+ return canReadResult; // canReadResult is already the boolean 'can' value from useCan
264
292
  },
265
293
  scope,
266
294
  isLoading,
267
295
  error
268
296
  }), [
269
297
  resource,
270
- canCreateResult,
271
- canUpdateResult,
272
- canDeleteResult,
273
- canReadResult,
298
+ canCreateResult, // This is already the boolean 'can' value
299
+ canUpdateResult, // This is already the boolean 'can' value
300
+ canDeleteResult, // This is already the boolean 'can' value
301
+ canReadResult, // This is already the boolean 'can' value
274
302
  enableRead,
275
303
  scope,
276
304
  isLoading,
@@ -106,35 +106,35 @@ export const EVENT_APP_PERMISSIONS = {
106
106
  // ============================================================================
107
107
 
108
108
  export const PAGE_PERMISSIONS = {
109
- // General page access
109
+ // General page access (generic - used for wildcard checks)
110
110
  READ_PAGE: 'read:page' as Permission,
111
111
  CREATE_PAGE: 'create:page' as Permission,
112
112
  UPDATE_PAGE: 'update:page' as Permission,
113
113
  DELETE_PAGE: 'delete:page' as Permission,
114
114
 
115
115
  // Admin pages
116
- READ_ADMIN: 'read:admin' as Permission,
117
- CREATE_ADMIN: 'create:admin' as Permission,
118
- UPDATE_ADMIN: 'update:admin' as Permission,
119
- DELETE_ADMIN: 'delete:admin' as Permission,
116
+ READ_ADMIN: 'read:page.admin' as Permission,
117
+ CREATE_ADMIN: 'create:page.admin' as Permission,
118
+ UPDATE_ADMIN: 'update:page.admin' as Permission,
119
+ DELETE_ADMIN: 'delete:page.admin' as Permission,
120
120
 
121
121
  // Dashboard pages
122
- READ_DASHBOARD: 'read:dashboard' as Permission,
123
- CREATE_DASHBOARD: 'create:dashboard' as Permission,
124
- UPDATE_DASHBOARD: 'update:dashboard' as Permission,
125
- DELETE_DASHBOARD: 'delete:dashboard' as Permission,
122
+ READ_DASHBOARD: 'read:page.dashboard' as Permission,
123
+ CREATE_DASHBOARD: 'create:page.dashboard' as Permission,
124
+ UPDATE_DASHBOARD: 'update:page.dashboard' as Permission,
125
+ DELETE_DASHBOARD: 'delete:page.dashboard' as Permission,
126
126
 
127
127
  // Settings pages
128
- READ_SETTINGS: 'read:settings' as Permission,
129
- CREATE_SETTINGS: 'create:settings' as Permission,
130
- UPDATE_SETTINGS: 'update:settings' as Permission,
131
- DELETE_SETTINGS: 'delete:settings' as Permission,
128
+ READ_SETTINGS: 'read:page.settings' as Permission,
129
+ CREATE_SETTINGS: 'create:page.settings' as Permission,
130
+ UPDATE_SETTINGS: 'update:page.settings' as Permission,
131
+ DELETE_SETTINGS: 'delete:page.settings' as Permission,
132
132
 
133
133
  // Reports pages
134
- READ_REPORTS: 'read:reports' as Permission,
135
- CREATE_REPORTS: 'create:reports' as Permission,
136
- UPDATE_REPORTS: 'update:reports' as Permission,
137
- DELETE_REPORTS: 'delete:reports' as Permission,
134
+ READ_REPORTS: 'read:page.reports' as Permission,
135
+ CREATE_REPORTS: 'create:page.reports' as Permission,
136
+ UPDATE_REPORTS: 'update:page.reports' as Permission,
137
+ DELETE_REPORTS: 'delete:page.reports' as Permission,
138
138
  } as const;
139
139
 
140
140
  // ============================================================================
@@ -89,7 +89,19 @@ export class ContextValidator {
89
89
  if (effectiveScopeType === 'both') {
90
90
  // For 'both' pages, we need at least one context (org or event)
91
91
  // Both will be checked during permission evaluation
92
+ // For PORTAL/ADMIN apps, both contexts are optional
92
93
  if (!scope.organisationId && !scope.eventId) {
94
+ if (allowsOptionalContexts(appName)) {
95
+ return {
96
+ isValid: true,
97
+ resolvedScope: {
98
+ organisationId: undefined,
99
+ eventId: undefined,
100
+ appId: scope.appId
101
+ },
102
+ error: null
103
+ };
104
+ }
93
105
  return {
94
106
  isValid: false,
95
107
  resolvedScope: null,
@@ -123,6 +135,18 @@ export class ContextValidator {
123
135
  // Handle 'event' scope - requires event context
124
136
  if (effectiveScopeType === 'event') {
125
137
  if (!scope.eventId) {
138
+ // For PORTAL/ADMIN apps, event context is optional
139
+ if (allowsOptionalContexts(appName)) {
140
+ return {
141
+ isValid: true,
142
+ resolvedScope: {
143
+ organisationId: scope.organisationId,
144
+ eventId: undefined,
145
+ appId: scope.appId
146
+ },
147
+ error: null
148
+ };
149
+ }
126
150
  return {
127
151
  isValid: false,
128
152
  resolvedScope: null,
@@ -167,6 +191,18 @@ export class ContextValidator {
167
191
  // Handle 'organisation' scope - requires organisation context
168
192
  if (effectiveScopeType === 'organisation') {
169
193
  if (!scope.organisationId) {
194
+ // For PORTAL/ADMIN apps, organisation context is optional
195
+ if (allowsOptionalContexts(appName)) {
196
+ return {
197
+ isValid: true,
198
+ resolvedScope: {
199
+ organisationId: undefined,
200
+ eventId: scope.eventId,
201
+ appId: scope.appId
202
+ },
203
+ error: null
204
+ };
205
+ }
170
206
  return {
171
207
  isValid: false,
172
208
  resolvedScope: null,
@@ -57,11 +57,8 @@ export class AuthService extends BaseService implements IAuthService {
57
57
 
58
58
  // Auth state getters
59
59
  getUser(): User | null {
60
- if (this.user) {
61
- logger.debug('AuthService', `getUser() [ID:${this.instanceId}] returning user: ${this.user.id}`);
62
- } else {
63
- logger.debug('AuthService', `getUser() [ID:${this.instanceId}] returning null`);
64
- }
60
+ // Removed debug logging - getUser() is called frequently and state changes
61
+ // are already logged in the auth state change handler
65
62
  return this.user;
66
63
  }
67
64
 
@@ -108,6 +108,9 @@ export class EventService extends BaseService implements IEventService {
108
108
  this.resetInitialization();
109
109
  this.isInitializedRef = false;
110
110
  this.isFetchingRef = false;
111
+ // Reset user cleared flag when new user logs in - allows auto-selection for new user
112
+ this.userClearedEventRef = false;
113
+ this.hasAutoSelectedRef = false;
111
114
 
112
115
  logger.debug('EventService', `User changed [ID:${this.instanceId}]`, {
113
116
  previousUserId,
@@ -177,15 +180,40 @@ export class EventService extends BaseService implements IEventService {
177
180
  // Do not clear events for super admins when organisation context is removed
178
181
  const shouldClearEvents = !this.isSuperAdmin;
179
182
 
183
+ // Determine if this is the first time an org is being set (from null/undefined to a value)
184
+ const isFirstOrgSet = (previousOrgId === null || previousOrgId === undefined) && newOrgId !== null && newOrgId !== undefined;
185
+
180
186
  // Clear events ONLY when switching between different organisations (not when org first becomes available)
181
- if (previousOrgId !== null && newOrgId !== null && previousOrgId !== newOrgId) {
187
+ // IMPORTANT: Check isFirstOrgSet FIRST to prevent clearing when org is first set
188
+ if (isFirstOrgSet) {
189
+ // Organisation first becomes available - DO NOT clear the event, preserve it
190
+ // The event will be validated when events are re-fetched with org context
191
+ // If it's no longer valid, it will be cleared without setting userClearedEventRef = true
192
+ const hadAutoSelectedEvent = this.hasAutoSelectedRef && !!this.selectedEvent;
193
+ this.userClearedEventRef = false;
194
+ // Don't reset hasAutoSelectedRef if we had an auto-selected event - preserve it
195
+ // This ensures the event remains selected when org is first set
196
+ if (!hadAutoSelectedEvent) {
197
+ this.hasAutoSelectedRef = false;
198
+ }
199
+ logger.debug('EventService', 'Organisation first set - preserving event and resetting auto-selection flags', {
200
+ organisationId: newOrgId,
201
+ hasSelectedEvent: !!this.selectedEvent,
202
+ selectedEventId: this.selectedEvent?.event_id,
203
+ hadAutoSelectedEvent,
204
+ preservingEvent: hadAutoSelectedEvent,
205
+ previousOrgId,
206
+ newOrgId
207
+ });
208
+ } else if (previousOrgId !== null && previousOrgId !== undefined && newOrgId !== null && newOrgId !== undefined && previousOrgId !== newOrgId) {
209
+ // Switching between different organisations - clear events
182
210
  if (shouldClearEvents) {
183
211
  this.events = [];
184
212
  // Use setSelectedEvent(null) to preserve userClearedEventRef flag if user explicitly cleared
185
213
  // This prevents auto-selection from re-selecting the event after org switch
186
214
  this.setSelectedEvent(null);
187
215
  }
188
- } else if (previousOrgId !== null && newOrgId === null) {
216
+ } else if (previousOrgId !== null && previousOrgId !== undefined && newOrgId === null) {
189
217
  // Organisation was removed - clear events if not super admin
190
218
  if (shouldClearEvents) {
191
219
  this.events = [];
@@ -232,7 +260,13 @@ export class EventService extends BaseService implements IEventService {
232
260
  });
233
261
  // Reset the user cleared flag when selecting an event
234
262
  this.userClearedEventRef = false;
263
+ logger.debug('EventService', 'Event selected', {
264
+ eventId: event.event_id,
265
+ eventName: event.event_name,
266
+ userClearedEventRef: this.userClearedEventRef
267
+ });
235
268
  } else {
269
+ const previousEventId = this.selectedEvent?.event_id;
236
270
  this.selectedEvent = null;
237
271
  this.setSelectedEventId?.(null);
238
272
  // Clear from secure storage (don't await to avoid blocking)
@@ -243,6 +277,11 @@ export class EventService extends BaseService implements IEventService {
243
277
  this.hasAutoSelectedRef = false;
244
278
  // Mark that user explicitly cleared the event to prevent auto-selection
245
279
  this.userClearedEventRef = true;
280
+ logger.debug('EventService', 'Event cleared via setSelectedEvent(null)', {
281
+ previousEventId,
282
+ userClearedEventRef: this.userClearedEventRef,
283
+ stackTrace: new Error().stack
284
+ });
246
285
  }
247
286
  this.notify();
248
287
  }
@@ -617,6 +656,28 @@ export class EventService extends BaseService implements IEventService {
617
656
  this.events = sortedEvents;
618
657
  this.error = null;
619
658
 
659
+ // Validate selected event - if it's no longer in the events list, clear it
660
+ // This can happen when org context changes or events are refreshed
661
+ // Don't set userClearedEventRef to true in this case - it's an automatic clear, not user-initiated
662
+ if (this.selectedEvent) {
663
+ const selectedEventId = this.selectedEvent.event_id;
664
+ const eventStillExists = transformedEvents.some(
665
+ e => e.event_id === selectedEventId
666
+ );
667
+ if (!eventStillExists) {
668
+ // Event no longer available - clear it but don't mark as user-cleared
669
+ const previousUserClearedRef = this.userClearedEventRef;
670
+ this.selectedEvent = null;
671
+ this.setSelectedEventId?.(null);
672
+ // Restore the previous userClearedEventRef value - this was an automatic clear, not user-initiated
673
+ this.userClearedEventRef = previousUserClearedRef;
674
+ logger.debug('EventService', 'Cleared selected event - no longer in events list', {
675
+ previousEventId: selectedEventId,
676
+ eventsCount: transformedEvents.length
677
+ });
678
+ }
679
+ }
680
+
620
681
  // Reset auto-selection ref for new events
621
682
  this.hasAutoSelectedRef = false;
622
683
 
@@ -624,26 +685,62 @@ export class EventService extends BaseService implements IEventService {
624
685
  if (!skipLoadPersisted) {
625
686
  const persistedEventLoaded = await this.loadPersistedEvent(transformedEvents);
626
687
 
688
+ logger.debug('EventService', 'Event selection check', {
689
+ persistedEventLoaded,
690
+ userClearedEventRef: this.userClearedEventRef,
691
+ eventsCount: transformedEvents.length,
692
+ hasSelectedEvent: !!this.selectedEvent
693
+ });
694
+
627
695
  // If no persisted event was loaded and user hasn't explicitly cleared an event, auto-select the next event
628
696
  if (!persistedEventLoaded && !this.userClearedEventRef) {
629
697
  const nextEvent = this.getNextEventByDate(transformedEvents);
698
+ logger.debug('EventService', 'Auto-selection attempt', {
699
+ nextEventFound: !!nextEvent,
700
+ nextEventId: nextEvent?.event_id,
701
+ nextEventDate: nextEvent?.event_date
702
+ });
630
703
  if (nextEvent) {
631
704
  this.hasAutoSelectedRef = true;
632
705
  // Use setSelectedEvent() to ensure consistent behavior
633
706
  // Theme will be applied by useEventTheme() hook
634
707
  this.setSelectedEvent(nextEvent);
708
+ logger.debug('EventService', 'Auto-selected next event', {
709
+ eventId: nextEvent.event_id,
710
+ eventName: nextEvent.event_name,
711
+ eventDate: nextEvent.event_date
712
+ });
713
+ } else {
714
+ logger.debug('EventService', 'No next event found for auto-selection', {
715
+ eventsCount: transformedEvents.length,
716
+ eventsWithDates: transformedEvents.filter(e => e.event_date).length
717
+ });
635
718
  }
719
+ } else if (persistedEventLoaded) {
720
+ logger.debug('EventService', 'Skipped auto-selection - persisted event loaded');
721
+ } else if (this.userClearedEventRef) {
722
+ logger.debug('EventService', 'Skipped auto-selection - user explicitly cleared event');
636
723
  }
637
724
  } else {
638
725
  // If skipping persisted event load, still do auto-selection for new users
639
726
  if (!this.userClearedEventRef) {
640
727
  const nextEvent = this.getNextEventByDate(transformedEvents);
728
+ logger.debug('EventService', 'Auto-selection attempt (skip persisted)', {
729
+ nextEventFound: !!nextEvent,
730
+ nextEventId: nextEvent?.event_id
731
+ });
641
732
  if (nextEvent) {
642
733
  this.hasAutoSelectedRef = true;
643
734
  // Use setSelectedEvent() to ensure consistent behavior
644
735
  // Theme will be applied by useEventTheme() hook
645
736
  this.setSelectedEvent(nextEvent);
737
+ logger.debug('EventService', 'Auto-selected next event (skip persisted)', {
738
+ eventId: nextEvent.event_id,
739
+ eventName: nextEvent.event_name
740
+ });
646
741
  }
742
+ } else {
743
+ logger.debug('EventService', 'Skipped auto-selection (skip persisted) - user explicitly cleared event');
647
744
  }
648
745
  }
649
746
  }