@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.
- package/README.md +2 -2
- package/dist/{DataTable-JXFCA2BJ.js → DataTable-SKCX4SCB.js} +6 -6
- package/dist/{EventLogo-rFL_kRjk.d.ts → EventLogo-B3V3otev.d.ts} +307 -1
- package/dist/{UnifiedAuthProvider-XIQQ7LVU.js → UnifiedAuthProvider-BMJAP6Z7.js} +3 -3
- package/dist/{chunk-22WKWKRX.js → chunk-2AKRP5QZ.js} +4 -4
- package/dist/{chunk-4C7EXCAR.js → chunk-CRGFNQ2L.js} +4 -4
- package/dist/{chunk-TLT2ZR3L.js → chunk-E6ZCVF4T.js} +4 -4
- package/dist/{chunk-INQLMHPF.js → chunk-ERGKJX4D.js} +2 -2
- package/dist/{chunk-6LAAY47Q.js → chunk-MSHEVJXS.js} +2 -2
- package/dist/{chunk-MA6EPSGZ.js → chunk-PKW27QVS.js} +2 -2
- package/dist/{chunk-T6JN6LH6.js → chunk-R53TUSFK.js} +3 -3
- package/dist/{chunk-6DXZ6V5Q.js → chunk-SFVL7ZFI.js} +5 -5
- package/dist/{chunk-5JMOHWDI.js → chunk-TUJSIWX6.js} +497 -329
- package/dist/chunk-TUJSIWX6.js.map +1 -0
- package/dist/{chunk-BOOI7GK2.js → chunk-VOJBGZYI.js} +119 -3
- package/dist/chunk-VOJBGZYI.js.map +1 -0
- package/dist/{chunk-YCWDTTUK.js → chunk-WM26XK7I.js} +22 -8
- package/dist/chunk-WM26XK7I.js.map +1 -0
- package/dist/components.d.ts +3 -1
- package/dist/components.js +20 -8
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.js +25 -11
- package/dist/index.js.map +1 -1
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +94 -1
- package/dist/rbac/index.js +9 -7
- package/dist/utils.js +1 -1
- package/docs/api/README.md +2 -2
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +40 -0
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +155 -0
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +9 -0
- package/docs/api/interfaces/TabsListProps.md +9 -0
- package/docs/api/interfaces/TabsProps.md +9 -0
- package/docs/api/interfaces/TabsTriggerProps.md +9 -0
- package/docs/api/interfaces/TextareaProps.md +53 -0
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +34 -0
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +289 -2
- package/docs/rbac/README.md +2 -1
- package/docs/rbac/event-based-apps.md +872 -0
- package/package.json +3 -1
- package/src/components/Calendar/Calendar.test.tsx +338 -0
- package/src/components/Calendar/Calendar.tsx +192 -0
- package/src/components/Calendar/index.ts +10 -0
- package/src/components/Tabs/Tabs.test.tsx +439 -0
- package/src/components/Tabs/Tabs.tsx +202 -0
- package/src/components/Tabs/index.ts +10 -0
- package/src/components/Textarea/Textarea.test.tsx +269 -0
- package/src/components/Textarea/Textarea.tsx +133 -0
- package/src/components/Textarea/index.ts +10 -0
- package/src/components/index.ts +11 -0
- package/src/index.ts +11 -0
- package/src/rbac/hooks/index.ts +2 -0
- package/src/rbac/hooks/useResourcePermissions.test.ts +633 -0
- package/src/rbac/hooks/useResourcePermissions.ts +235 -0
- package/src/services/EventService.ts +29 -8
- package/src/services/__tests__/EventService.test.ts +48 -8
- package/dist/chunk-5JMOHWDI.js.map +0 -1
- package/dist/chunk-BOOI7GK2.js.map +0 -1
- package/dist/chunk-YCWDTTUK.js.map +0 -1
- package/src/rbac/docs/event-based-apps.md +0 -285
- /package/dist/{DataTable-JXFCA2BJ.js.map → DataTable-SKCX4SCB.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-XIQQ7LVU.js.map → UnifiedAuthProvider-BMJAP6Z7.js.map} +0 -0
- /package/dist/{chunk-22WKWKRX.js.map → chunk-2AKRP5QZ.js.map} +0 -0
- /package/dist/{chunk-4C7EXCAR.js.map → chunk-CRGFNQ2L.js.map} +0 -0
- /package/dist/{chunk-TLT2ZR3L.js.map → chunk-E6ZCVF4T.js.map} +0 -0
- /package/dist/{chunk-INQLMHPF.js.map → chunk-ERGKJX4D.js.map} +0 -0
- /package/dist/{chunk-6LAAY47Q.js.map → chunk-MSHEVJXS.js.map} +0 -0
- /package/dist/{chunk-MA6EPSGZ.js.map → chunk-PKW27QVS.js.map} +0 -0
- /package/dist/{chunk-T6JN6LH6.js.map → chunk-R53TUSFK.js.map} +0 -0
- /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
|
|
438
|
-
|
|
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
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
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
|
|
271
|
-
const
|
|
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: [
|
|
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
|
-
|
|
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
|
|
764
|
-
const
|
|
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([
|
|
783
|
+
const nextEvent = eventService.getNextEventByDate([pastEvent1, pastEvent2]);
|
|
770
784
|
|
|
771
|
-
|
|
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,
|