@jmruthers/pace-core 0.5.140 → 0.5.142

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 (180) hide show
  1. package/README.md +2 -2
  2. package/dist/{DataTable-JXFCA2BJ.js → DataTable-SKCX4SCB.js} +6 -6
  3. package/dist/{EventLogo-rFL_kRjk.d.ts → EventLogo-B3V3otev.d.ts} +307 -1
  4. package/dist/{UnifiedAuthProvider-XIQQ7LVU.js → UnifiedAuthProvider-BMJAP6Z7.js} +3 -3
  5. package/dist/{chunk-22WKWKRX.js → chunk-2AKRP5QZ.js} +4 -4
  6. package/dist/{chunk-4C7EXCAR.js → chunk-CRGFNQ2L.js} +4 -4
  7. package/dist/{chunk-TLT2ZR3L.js → chunk-E6ZCVF4T.js} +4 -4
  8. package/dist/{chunk-INQLMHPF.js → chunk-ERGKJX4D.js} +2 -2
  9. package/dist/{chunk-6LAAY47Q.js → chunk-MSHEVJXS.js} +2 -2
  10. package/dist/{chunk-MA6EPSGZ.js → chunk-PKW27QVS.js} +2 -2
  11. package/dist/{chunk-T6JN6LH6.js → chunk-R53TUSFK.js} +3 -3
  12. package/dist/{chunk-6DXZ6V5Q.js → chunk-SFVL7ZFI.js} +5 -5
  13. package/dist/{chunk-5JMOHWDI.js → chunk-TUJSIWX6.js} +497 -329
  14. package/dist/chunk-TUJSIWX6.js.map +1 -0
  15. package/dist/{chunk-BOOI7GK2.js → chunk-VOJBGZYI.js} +119 -3
  16. package/dist/chunk-VOJBGZYI.js.map +1 -0
  17. package/dist/{chunk-YCWDTTUK.js → chunk-WM26XK7I.js} +22 -8
  18. package/dist/chunk-WM26XK7I.js.map +1 -0
  19. package/dist/components.d.ts +3 -1
  20. package/dist/components.js +20 -8
  21. package/dist/components.js.map +1 -1
  22. package/dist/hooks.js +7 -7
  23. package/dist/index.d.ts +4 -2
  24. package/dist/index.js +25 -11
  25. package/dist/index.js.map +1 -1
  26. package/dist/providers.js +2 -2
  27. package/dist/rbac/index.d.ts +94 -1
  28. package/dist/rbac/index.js +9 -7
  29. package/dist/utils.js +1 -1
  30. package/docs/api/README.md +2 -2
  31. package/docs/api/classes/ColumnFactory.md +1 -1
  32. package/docs/api/classes/ErrorBoundary.md +1 -1
  33. package/docs/api/classes/InvalidScopeError.md +1 -1
  34. package/docs/api/classes/MissingUserContextError.md +1 -1
  35. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  36. package/docs/api/classes/PermissionDeniedError.md +1 -1
  37. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  38. package/docs/api/classes/RBACAuditManager.md +1 -1
  39. package/docs/api/classes/RBACCache.md +1 -1
  40. package/docs/api/classes/RBACEngine.md +1 -1
  41. package/docs/api/classes/RBACError.md +1 -1
  42. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  43. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  44. package/docs/api/classes/StorageUtils.md +1 -1
  45. package/docs/api/enums/FileCategory.md +1 -1
  46. package/docs/api/interfaces/AggregateConfig.md +1 -1
  47. package/docs/api/interfaces/BadgeProps.md +1 -1
  48. package/docs/api/interfaces/ButtonProps.md +1 -1
  49. package/docs/api/interfaces/CalendarProps.md +40 -0
  50. package/docs/api/interfaces/CardProps.md +1 -1
  51. package/docs/api/interfaces/ColorPalette.md +1 -1
  52. package/docs/api/interfaces/ColorShade.md +1 -1
  53. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  54. package/docs/api/interfaces/DataRecord.md +1 -1
  55. package/docs/api/interfaces/DataTableAction.md +1 -1
  56. package/docs/api/interfaces/DataTableColumn.md +1 -1
  57. package/docs/api/interfaces/DataTableProps.md +1 -1
  58. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  59. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  60. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  61. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  62. package/docs/api/interfaces/EventLogoProps.md +1 -1
  63. package/docs/api/interfaces/ExportColumn.md +1 -1
  64. package/docs/api/interfaces/ExportOptions.md +1 -1
  65. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  66. package/docs/api/interfaces/FileMetadata.md +1 -1
  67. package/docs/api/interfaces/FileReference.md +1 -1
  68. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  69. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  70. package/docs/api/interfaces/FileUploadProps.md +1 -1
  71. package/docs/api/interfaces/FooterProps.md +1 -1
  72. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  73. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  74. package/docs/api/interfaces/InputProps.md +1 -1
  75. package/docs/api/interfaces/LabelProps.md +1 -1
  76. package/docs/api/interfaces/LoginFormProps.md +1 -1
  77. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  78. package/docs/api/interfaces/NavigationContextType.md +1 -1
  79. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  80. package/docs/api/interfaces/NavigationItem.md +1 -1
  81. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  82. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  83. package/docs/api/interfaces/Organisation.md +1 -1
  84. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  85. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  86. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  87. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  88. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  89. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  90. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  91. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  92. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  93. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  94. package/docs/api/interfaces/PaletteData.md +1 -1
  95. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  96. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  97. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  98. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  99. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  100. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  101. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  102. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  103. package/docs/api/interfaces/RBACConfig.md +1 -1
  104. package/docs/api/interfaces/RBACLogger.md +1 -1
  105. package/docs/api/interfaces/ResourcePermissions.md +155 -0
  106. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  107. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  108. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  109. package/docs/api/interfaces/RoleManagementResult.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/SessionRestorationLoaderProps.md +1 -1
  115. package/docs/api/interfaces/StorageConfig.md +1 -1
  116. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  117. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  118. package/docs/api/interfaces/StorageListOptions.md +1 -1
  119. package/docs/api/interfaces/StorageListResult.md +1 -1
  120. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  121. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  122. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  123. package/docs/api/interfaces/StyleImport.md +1 -1
  124. package/docs/api/interfaces/SwitchProps.md +1 -1
  125. package/docs/api/interfaces/TabsContentProps.md +9 -0
  126. package/docs/api/interfaces/TabsListProps.md +9 -0
  127. package/docs/api/interfaces/TabsProps.md +9 -0
  128. package/docs/api/interfaces/TabsTriggerProps.md +9 -0
  129. package/docs/api/interfaces/TextareaProps.md +53 -0
  130. package/docs/api/interfaces/ToastActionElement.md +1 -1
  131. package/docs/api/interfaces/ToastProps.md +1 -1
  132. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  133. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  134. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  135. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  136. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  137. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  138. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  139. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  140. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  141. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  142. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  143. package/docs/api/interfaces/UseResourcePermissionsOptions.md +34 -0
  144. package/docs/api/interfaces/UserEventAccess.md +1 -1
  145. package/docs/api/interfaces/UserMenuProps.md +1 -1
  146. package/docs/api/interfaces/UserProfile.md +1 -1
  147. package/docs/api/modules.md +289 -2
  148. package/docs/rbac/README.md +2 -1
  149. package/docs/rbac/event-based-apps.md +872 -0
  150. package/package.json +3 -1
  151. package/src/components/Calendar/Calendar.test.tsx +338 -0
  152. package/src/components/Calendar/Calendar.tsx +192 -0
  153. package/src/components/Calendar/index.ts +10 -0
  154. package/src/components/Tabs/Tabs.test.tsx +439 -0
  155. package/src/components/Tabs/Tabs.tsx +202 -0
  156. package/src/components/Tabs/index.ts +10 -0
  157. package/src/components/Textarea/Textarea.test.tsx +269 -0
  158. package/src/components/Textarea/Textarea.tsx +133 -0
  159. package/src/components/Textarea/index.ts +10 -0
  160. package/src/components/index.ts +11 -0
  161. package/src/index.ts +11 -0
  162. package/src/rbac/hooks/index.ts +2 -0
  163. package/src/rbac/hooks/useResourcePermissions.test.ts +633 -0
  164. package/src/rbac/hooks/useResourcePermissions.ts +235 -0
  165. package/src/services/EventService.ts +29 -8
  166. package/src/services/__tests__/EventService.test.ts +48 -8
  167. package/dist/chunk-5JMOHWDI.js.map +0 -1
  168. package/dist/chunk-BOOI7GK2.js.map +0 -1
  169. package/dist/chunk-YCWDTTUK.js.map +0 -1
  170. package/src/rbac/docs/event-based-apps.md +0 -285
  171. /package/dist/{DataTable-JXFCA2BJ.js.map → DataTable-SKCX4SCB.js.map} +0 -0
  172. /package/dist/{UnifiedAuthProvider-XIQQ7LVU.js.map → UnifiedAuthProvider-BMJAP6Z7.js.map} +0 -0
  173. /package/dist/{chunk-22WKWKRX.js.map → chunk-2AKRP5QZ.js.map} +0 -0
  174. /package/dist/{chunk-4C7EXCAR.js.map → chunk-CRGFNQ2L.js.map} +0 -0
  175. /package/dist/{chunk-TLT2ZR3L.js.map → chunk-E6ZCVF4T.js.map} +0 -0
  176. /package/dist/{chunk-INQLMHPF.js.map → chunk-ERGKJX4D.js.map} +0 -0
  177. /package/dist/{chunk-6LAAY47Q.js.map → chunk-MSHEVJXS.js.map} +0 -0
  178. /package/dist/{chunk-MA6EPSGZ.js.map → chunk-PKW27QVS.js.map} +0 -0
  179. /package/dist/{chunk-T6JN6LH6.js.map → chunk-R53TUSFK.js.map} +0 -0
  180. /package/dist/{chunk-6DXZ6V5Q.js.map → chunk-SFVL7ZFI.js.map} +0 -0
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @file useResourcePermissions Hook
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/Hooks
5
+ * @since 1.0.0
6
+ *
7
+ * Hook to check permissions for a specific resource type.
8
+ * This hook centralizes the common pattern of checking create/update/delete/read
9
+ * permissions, eliminating ~30 lines of boilerplate code per hook usage.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * import { useResourcePermissions } from '@jmruthers/pace-core/rbac';
14
+ *
15
+ * function ContactsHook() {
16
+ * const { canCreate, canUpdate, canDelete } = useResourcePermissions('contacts');
17
+ *
18
+ * const addContact = async (data: ContactData) => {
19
+ * if (!canCreate('contacts')) {
20
+ * throw new Error("Permission denied: You do not have permission to create contacts.");
21
+ * }
22
+ * // ... perform mutation
23
+ * };
24
+ * }
25
+ * ```
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * // With read permissions enabled
30
+ * const { canRead } = useResourcePermissions('contacts', { enableRead: true });
31
+ *
32
+ * if (!canRead('contacts')) {
33
+ * return <PermissionDenied />;
34
+ * }
35
+ * ```
36
+ *
37
+ * @security
38
+ * - Requires organisation context (handled by useResolvedScope)
39
+ * - All permission checks are scoped to the current organisation/event/app context
40
+ * - Missing user context results in all permissions being denied
41
+ */
42
+
43
+ import { useMemo } from 'react';
44
+ import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
45
+ import { useOrganisations } from '../../hooks/useOrganisations';
46
+ import { useEvents } from '../../hooks/useEvents';
47
+ import { useResolvedScope } from './useResolvedScope';
48
+ import { useCan } from './usePermissions';
49
+ import type { Scope } from '../types';
50
+
51
+ export interface UseResourcePermissionsOptions {
52
+ /** Whether to check read permissions (default: false) */
53
+ enableRead?: boolean;
54
+ /** Whether scope resolution is required (default: true) */
55
+ requireScope?: boolean;
56
+ }
57
+
58
+ export interface ResourcePermissions {
59
+ /** Check if user can create resources of this type */
60
+ canCreate: (resource: string) => boolean;
61
+ /** Check if user can update resources of this type */
62
+ canUpdate: (resource: string) => boolean;
63
+ /** Check if user can delete resources of this type */
64
+ canDelete: (resource: string) => boolean;
65
+ /** Check if user can read resources of this type */
66
+ canRead: (resource: string) => boolean;
67
+ /** The resolved scope object (for advanced use cases) */
68
+ scope: Scope;
69
+ /** Whether any permission check is currently loading */
70
+ isLoading: boolean;
71
+ /** Error from any permission check or scope resolution */
72
+ error: Error | null;
73
+ }
74
+
75
+ /**
76
+ * Hook to check permissions for a specific resource
77
+ *
78
+ * This hook encapsulates the common pattern of checking create/update/delete/read
79
+ * permissions for a resource type. It handles scope resolution, user context,
80
+ * and provides a simple API for permission checking.
81
+ *
82
+ * @param resource - The resource name (e.g., 'contacts', 'risks', 'journal')
83
+ * @param options - Optional configuration
84
+ * @param options.enableRead - Whether to check read permissions (default: false)
85
+ * @param options.requireScope - Whether scope resolution is required (default: true)
86
+ * @returns Object with permission check functions and scope
87
+ *
88
+ * @example
89
+ * ```tsx
90
+ * function useContacts() {
91
+ * const { canCreate, canUpdate, canDelete } = useResourcePermissions('contacts');
92
+ *
93
+ * const addContact = async (data: ContactData) => {
94
+ * if (!canCreate('contacts')) {
95
+ * throw new Error("Permission denied");
96
+ * }
97
+ * // ... perform mutation
98
+ * };
99
+ * }
100
+ * ```
101
+ */
102
+ export function useResourcePermissions(
103
+ resource: string,
104
+ options: UseResourcePermissionsOptions = {}
105
+ ): ResourcePermissions {
106
+ const { enableRead = false, requireScope = true } = options;
107
+
108
+ // Get user and supabase client from UnifiedAuth
109
+ const { user, supabase } = useUnifiedAuth();
110
+
111
+ // Get selected organisation
112
+ const { selectedOrganisation } = useOrganisations();
113
+
114
+ // Get selected event (optional - wrap in try/catch)
115
+ let selectedEvent: { event_id: string } | null = null;
116
+ try {
117
+ const eventsContext = useEvents();
118
+ selectedEvent = eventsContext.selectedEvent;
119
+ } catch (error) {
120
+ // Event provider not available - continue without event context
121
+ // This is expected in some apps that don't use events
122
+ }
123
+
124
+ // Resolve scope for permission checks
125
+ const { resolvedScope, isLoading: scopeLoading, error: scopeError } = useResolvedScope({
126
+ supabase,
127
+ selectedOrganisationId: selectedOrganisation?.id || null,
128
+ selectedEventId: selectedEvent?.event_id || null
129
+ });
130
+
131
+ // Create fallback scope if resolvedScope is not available
132
+ const scope: Scope = resolvedScope || {
133
+ organisationId: selectedOrganisation?.id || '',
134
+ eventId: selectedEvent?.event_id || undefined,
135
+ appId: undefined
136
+ };
137
+
138
+ // Permission checks for create, update, delete
139
+ const { can: canCreateResult, isLoading: createLoading, error: createError } = useCan(
140
+ user?.id || '',
141
+ scope,
142
+ `create:${resource}` as const,
143
+ undefined, // pageId
144
+ true // useCache
145
+ );
146
+
147
+ const { can: canUpdateResult, isLoading: updateLoading, error: updateError } = useCan(
148
+ user?.id || '',
149
+ scope,
150
+ `update:${resource}` as const,
151
+ undefined, // pageId
152
+ true // useCache
153
+ );
154
+
155
+ const { can: canDeleteResult, isLoading: deleteLoading, error: deleteError } = useCan(
156
+ user?.id || '',
157
+ scope,
158
+ `delete:${resource}` as const,
159
+ undefined, // pageId
160
+ true // useCache
161
+ );
162
+
163
+ // Optional read permission check
164
+ const { can: canReadResult, isLoading: readLoading, error: readError } = useCan(
165
+ user?.id || '',
166
+ scope,
167
+ `read:${resource}` as const,
168
+ undefined, // pageId
169
+ true // useCache
170
+ );
171
+
172
+ // Aggregate loading states - any permission check or scope resolution loading
173
+ const isLoading = useMemo(() => {
174
+ return scopeLoading || createLoading || updateLoading || deleteLoading || (enableRead && readLoading);
175
+ }, [scopeLoading, createLoading, updateLoading, deleteLoading, readLoading, enableRead]);
176
+
177
+ // Aggregate errors - prefer scope error, then any permission error
178
+ const error = useMemo(() => {
179
+ if (scopeError) return scopeError;
180
+ if (createError) return createError;
181
+ if (updateError) return updateError;
182
+ if (deleteError) return deleteError;
183
+ if (enableRead && readError) return readError;
184
+ return null;
185
+ }, [scopeError, createError, updateError, deleteError, readError, enableRead]);
186
+
187
+ // Return wrapper functions that take resource name and return permission result
188
+ // Note: The resource parameter in the function is for consistency with the API,
189
+ // but we're checking permissions for the resource passed to the hook
190
+ return useMemo(() => ({
191
+ canCreate: (res: string) => {
192
+ // For now, we only check the resource passed to the hook
193
+ // Future enhancement could support checking different resources
194
+ if (res !== resource) {
195
+ return false;
196
+ }
197
+ return canCreateResult;
198
+ },
199
+ canUpdate: (res: string) => {
200
+ if (res !== resource) {
201
+ return false;
202
+ }
203
+ return canUpdateResult;
204
+ },
205
+ canDelete: (res: string) => {
206
+ if (res !== resource) {
207
+ return false;
208
+ }
209
+ return canDeleteResult;
210
+ },
211
+ canRead: (res: string) => {
212
+ if (!enableRead) {
213
+ return true; // If read checking is disabled, allow read
214
+ }
215
+ if (res !== resource) {
216
+ return false;
217
+ }
218
+ return canReadResult;
219
+ },
220
+ scope,
221
+ isLoading,
222
+ error
223
+ }), [
224
+ resource,
225
+ canCreateResult,
226
+ canUpdateResult,
227
+ canDeleteResult,
228
+ canReadResult,
229
+ enableRead,
230
+ scope,
231
+ isLoading,
232
+ error
233
+ ]);
234
+ }
235
+
@@ -434,17 +434,38 @@ export class EventService extends BaseService implements IEventService {
434
434
  return startOfEventDate >= startOfToday;
435
435
  });
436
436
 
437
- if (futureEvents.length === 0) {
438
- return null;
437
+ if (futureEvents.length > 0) {
438
+ // Sort by date (ascending) to get the next event
439
+ const sortedFutureEvents = futureEvents.sort((a, b) => {
440
+ const dateA = new Date(a.event_date!);
441
+ const dateB = new Date(b.event_date!);
442
+ return dateA.getTime() - dateB.getTime();
443
+ });
444
+
445
+ return sortedFutureEvents[0];
439
446
  }
440
447
 
441
- // Sort by date (ascending) to get the next event
442
- const sortedFutureEvents = futureEvents.sort((a, b) => {
443
- const dateA = new Date(a.event_date!);
444
- const dateB = new Date(b.event_date!);
445
- return dateA.getTime() - dateB.getTime();
448
+ // Fallback: If no future events found, return the most recent past event
449
+ // This handles cases where users only have access to past events
450
+ const pastEvents = eventsToUse.filter(event => {
451
+ if (!event.event_date) return false;
452
+ const eventDate = new Date(event.event_date);
453
+ const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
454
+ return startOfEventDate < startOfToday;
446
455
  });
447
456
 
448
- return sortedFutureEvents[0];
457
+ if (pastEvents.length > 0) {
458
+ // Sort by date (descending) to get the most recent past event
459
+ const sortedPastEvents = pastEvents.sort((a, b) => {
460
+ const dateA = new Date(a.event_date!);
461
+ const dateB = new Date(b.event_date!);
462
+ return dateB.getTime() - dateA.getTime(); // Descending order
463
+ });
464
+
465
+ return sortedPastEvents[0];
466
+ }
467
+
468
+ // No events found at all
469
+ return null;
449
470
  }
450
471
  }
@@ -267,14 +267,20 @@ describe('EventService', () => {
267
267
  expect(nextEvent).toEqual(mockEvent2); // Future event should be selected
268
268
  });
269
269
 
270
- it('should return null when no future events', async () => {
271
- const pastEvent: Event = {
270
+ it('should return most recent past event when no future events', async () => {
271
+ const pastEvent1: Event = {
272
272
  ...mockEvent,
273
+ event_id: 'event-past-1',
273
274
  event_date: '2020-01-01T00:00:00Z'
274
275
  };
276
+ const pastEvent2: Event = {
277
+ ...mockEvent,
278
+ event_id: 'event-past-2',
279
+ event_date: '2021-06-15T00:00:00Z' // More recent past event
280
+ };
275
281
 
276
282
  mockSupabase.rpc.mockResolvedValue({
277
- data: [pastEvent],
283
+ data: [pastEvent1, pastEvent2],
278
284
  error: null
279
285
  });
280
286
 
@@ -290,7 +296,9 @@ describe('EventService', () => {
290
296
  await service.initialize();
291
297
 
292
298
  const nextEvent = service.getNextEventByDate();
293
- expect(nextEvent).toBeNull();
299
+ // Should return the most recent past event (pastEvent2)
300
+ expect(nextEvent).not.toBeNull();
301
+ expect(nextEvent?.event_id).toBe('event-past-2');
294
302
  });
295
303
 
296
304
  it('should persist event selection', async () => {
@@ -760,15 +768,23 @@ describe('EventService', () => {
760
768
  expect(nextEvent).toBeNull();
761
769
  });
762
770
 
763
- it('should return null when all events are in the past', () => {
764
- const pastEvent: Event = {
771
+ it('should return most recent past event when all events are in the past', () => {
772
+ const pastEvent1: Event = {
765
773
  ...mockEvent,
774
+ event_id: 'event-past-1',
766
775
  event_date: '2020-01-01T00:00:00Z'
767
776
  };
777
+ const pastEvent2: Event = {
778
+ ...mockEvent,
779
+ event_id: 'event-past-2',
780
+ event_date: '2022-03-20T00:00:00Z' // More recent past event
781
+ };
768
782
 
769
- const nextEvent = eventService.getNextEventByDate([pastEvent]);
783
+ const nextEvent = eventService.getNextEventByDate([pastEvent1, pastEvent2]);
770
784
 
771
- expect(nextEvent).toBeNull();
785
+ // Should return the most recent past event (pastEvent2)
786
+ expect(nextEvent).not.toBeNull();
787
+ expect(nextEvent?.event_id).toBe('event-past-2');
772
788
  });
773
789
 
774
790
  it('should select event on today\'s date', () => {
@@ -826,6 +842,30 @@ describe('EventService', () => {
826
842
  expect(nextEvent?.event_date).toBe(futureDate2.toISOString());
827
843
  });
828
844
 
845
+ it('should prefer future events over past events when both exist', () => {
846
+ const today = new Date();
847
+ const pastDate = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
848
+ const futureDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days from now
849
+
850
+ const pastEvent: Event = {
851
+ ...mockEvent,
852
+ event_id: 'event-past',
853
+ event_date: pastDate.toISOString()
854
+ };
855
+
856
+ const futureEvent: Event = {
857
+ ...mockEvent,
858
+ event_id: 'event-future',
859
+ event_date: futureDate.toISOString()
860
+ };
861
+
862
+ const nextEvent = eventService.getNextEventByDate([pastEvent, futureEvent]);
863
+
864
+ // Should prefer future event even if past event is more recent
865
+ expect(nextEvent).not.toBeNull();
866
+ expect(nextEvent?.event_id).toBe('event-future');
867
+ });
868
+
829
869
  it('should filter out events without dates', () => {
830
870
  const eventWithoutDate: Event = {
831
871
  ...mockEvent,