@jmruthers/pace-core 0.5.184 → 0.5.186
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/CHANGELOG.md +38 -0
- package/README.md +60 -1
- package/core-usage-manifest.json +312 -0
- package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
- package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
- package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-DIzEzwKl.d.ts} +23 -10
- package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
- package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
- package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
- package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
- package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
- package/dist/{chunk-W22JP75J.js → chunk-DAGICKHT.js} +9 -7
- package/dist/chunk-DAGICKHT.js.map +1 -0
- package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
- package/dist/chunk-FXFJRTKI.js.map +1 -0
- package/dist/{chunk-CSOFYHAG.js → chunk-GRIQLQ52.js} +374 -60
- package/dist/chunk-GRIQLQ52.js.map +1 -0
- package/dist/{chunk-NQPMQGS2.js → chunk-HDCUMOOI.js} +497 -399
- package/dist/chunk-HDCUMOOI.js.map +1 -0
- package/dist/chunk-HESYZWZW.js +388 -0
- package/dist/chunk-HESYZWZW.js.map +1 -0
- package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
- package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
- package/dist/{chunk-PWAHJW4G.js → chunk-OALXJH4Y.js} +86 -33
- package/dist/chunk-OALXJH4Y.js.map +1 -0
- package/dist/{chunk-MI7HBHN3.js → chunk-TC7D3CR3.js} +89 -9
- package/dist/chunk-TC7D3CR3.js.map +1 -0
- package/dist/chunk-THRPYOFK.js +215 -0
- package/dist/chunk-THRPYOFK.js.map +1 -0
- package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
- package/dist/chunk-U6WNSFX5.js.map +1 -0
- package/dist/{chunk-UHNYIBXL.js → chunk-UQWSHFVX.js} +1 -1
- package/dist/chunk-UQWSHFVX.js.map +1 -0
- package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
- package/dist/components.d.ts +182 -6
- package/dist/components.js +157 -11
- package/dist/components.js.map +1 -1
- package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
- package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
- package/dist/{file-reference-D06mEEWW.d.ts → file-reference-PRTSLxKx.d.ts} +10 -1
- package/dist/hooks.d.ts +52 -15
- package/dist/hooks.js +12 -22
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.js +82 -18
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +3 -1
- package/dist/rbac/index.d.ts +206 -15
- package/dist/rbac/index.js +28 -6
- package/dist/timezone-_pgH8qrY.d.ts +530 -0
- package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +114 -3
- package/dist/utils.d.ts +110 -152
- package/dist/utils.js +128 -138
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +60 -1
- 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/Logger.md +178 -0
- 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/RBACAuditManager.md +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +5 -5
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +54 -0
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.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 +18 -2
- 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/ComplianceResult.md +30 -0
- 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/DatabaseComplianceResult.md +85 -0
- package/docs/api/interfaces/DatabaseIssue.md +41 -0
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +6 -6
- 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 +48 -8
- package/docs/api/interfaces/FileUploadProps.md +46 -13
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
- 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/LoggerConfig.md +62 -0
- 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 +36 -23
- 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 +11 -11
- 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/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
- 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/QuickFix.md +52 -0
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +4 -4
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +5 -5
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
- 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/SetupIssue.md +41 -0
- 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 +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.md +1 -1
- 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/UseFormDialogOptions.md +62 -0
- package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- package/docs/api/interfaces/UsePublicEventLogoReturn.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 +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- 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 +746 -50
- package/docs/api-reference/components.md +26 -12
- package/docs/api-reference/hooks.md +111 -0
- package/docs/api-reference/rpc-functions.md +1 -1
- package/docs/api-reference/utilities.md +184 -0
- package/docs/getting-started/installation-guide.md +75 -16
- package/docs/getting-started/quick-start.md +61 -11
- package/docs/implementation-guides/authentication.md +88 -12
- package/docs/implementation-guides/file-reference-system.md +26 -3
- package/docs/implementation-guides/file-upload-storage.md +30 -1
- package/docs/rbac/README.md +1 -0
- package/docs/rbac/compliance/compliance-guide.md +544 -0
- package/docs/rbac/getting-started.md +158 -33
- package/docs/standards/pace-core-compliance.md +432 -0
- package/eslint-config-pace-core.cjs +93 -0
- package/package.json +15 -3
- package/scripts/analyze-bundle.js +232 -0
- package/scripts/build-css.js +56 -0
- package/scripts/build-docs-incremental.js +1015 -0
- package/scripts/check-pace-core-compliance.cjs +2353 -0
- package/scripts/check-pace-core-compliance.js +512 -0
- package/scripts/generate-docs.js +157 -0
- package/scripts/setup-build-cache.js +73 -0
- package/scripts/utils/command-runner.js +131 -0
- package/scripts/utils/env.js +33 -0
- package/scripts/utils/index.js +10 -0
- package/scripts/utils/logger.js +88 -0
- package/scripts/utils/path-helpers.js +37 -0
- package/scripts/validate-formats.js +133 -0
- package/scripts/validate-master.js +155 -0
- package/scripts/validate-pre-publish.js +140 -0
- package/scripts/validate-theme.js +142 -0
- package/src/components/Calendar/Calendar.tsx +8 -1
- package/src/components/Card/Card.tsx +47 -8
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
- package/src/components/DatePickerWithTimezone/README.md +135 -0
- package/src/components/DatePickerWithTimezone/index.ts +10 -0
- package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
- package/src/components/DateTimeField/DateTimeField.tsx +232 -0
- package/src/components/DateTimeField/README.md +148 -0
- package/src/components/DateTimeField/index.ts +10 -0
- package/src/components/FileUpload/FileUpload.test.tsx +2 -0
- package/src/components/FileUpload/FileUpload.tsx +10 -1
- package/src/components/Header/Header.test.tsx +47 -18
- package/src/components/Header/Header.tsx +22 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
- package/src/components/PaceAppLayout/README.md +9 -0
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +146 -5
- package/src/components/index.ts +8 -0
- package/src/eslint-rules/pace-core-compliance.cjs +406 -0
- package/src/eslint-rules/pace-core-compliance.js +640 -0
- package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useFileReference.test.ts +2 -0
- package/src/hooks/useFormDialog.ts +147 -0
- package/src/hooks/usePreventTabReload.ts +106 -0
- package/src/hooks/useSecureDataAccess.ts +2 -2
- package/src/index.ts +27 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
- package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
- package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
- package/src/rbac/compliance/database-validator.ts +165 -0
- package/src/rbac/compliance/index.ts +38 -0
- package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
- package/src/rbac/compliance/runtime-compliance.ts +77 -0
- package/src/rbac/compliance/setup-validator.ts +131 -0
- package/src/rbac/components/PagePermissionGuard.tsx +8 -64
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
- package/src/rbac/docs/event-based-apps.md +285 -0
- package/src/rbac/errors.ts +11 -0
- package/src/rbac/hooks/useRoleManagement.ts +292 -12
- package/src/rbac/index.ts +30 -0
- package/src/services/OrganisationService.ts +4 -0
- package/src/styles/core.css +5 -5
- package/src/types/database.generated.ts +63 -9
- package/src/types/file-reference.ts +9 -0
- package/src/utils/__tests__/timezone.test.ts +345 -0
- package/src/utils/file-reference/__tests__/file-reference.test.ts +60 -4
- package/src/utils/file-reference/index.ts +13 -2
- package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
- package/src/utils/formatting/formatting.ts +179 -0
- package/src/utils/index.ts +27 -1
- package/src/utils/location/index.ts +16 -0
- package/src/utils/location/location.test.ts +286 -0
- package/src/utils/location/location.ts +175 -0
- package/src/utils/security/secureDataAccess.ts +1 -1
- package/src/utils/storage/helpers.ts +68 -0
- package/src/utils/timezone/index.ts +17 -0
- package/src/utils/timezone/timezone.test.ts +349 -0
- package/src/utils/timezone/timezone.ts +281 -0
- package/dist/chunk-CSOFYHAG.js.map +0 -1
- package/dist/chunk-FUEYYMX5.js.map +0 -1
- package/dist/chunk-HKIT6O7W.js +0 -198
- package/dist/chunk-HKIT6O7W.js.map +0 -1
- package/dist/chunk-KUEN3HFB.js +0 -94
- package/dist/chunk-KUEN3HFB.js.map +0 -1
- package/dist/chunk-M7W4CP3M.js.map +0 -1
- package/dist/chunk-MI7HBHN3.js.map +0 -1
- package/dist/chunk-NQPMQGS2.js.map +0 -1
- package/dist/chunk-PWAHJW4G.js.map +0 -1
- package/dist/chunk-UHNYIBXL.js.map +0 -1
- package/dist/chunk-W22JP75J.js.map +0 -1
- package/dist/formatting-5wETwiGF.d.ts +0 -162
- /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
- /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
- /package/dist/{chunk-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Location Utilities
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Location
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Utility functions for working with geographic coordinates.
|
|
8
|
+
* Provides functions for formatting, validating, comparing, and generating URLs for coordinates.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Format coordinates for display
|
|
12
|
+
* - Validate coordinate objects
|
|
13
|
+
* - Compare coordinates with tolerance
|
|
14
|
+
* - Generate Google Maps URLs
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { formatCoordinates, hasValidCoordinates, areCoordinatesEqual, getGoogleMapsUrl } from '@jmruthers/pace-core/utils/location';
|
|
19
|
+
*
|
|
20
|
+
* const coords = { lat: -37.8136, lng: 144.9631 };
|
|
21
|
+
*
|
|
22
|
+
* // Format for display
|
|
23
|
+
* formatCoordinates(coords); // "-37.813600, 144.963100"
|
|
24
|
+
*
|
|
25
|
+
* // Validate
|
|
26
|
+
* hasValidCoordinates(coords); // true
|
|
27
|
+
*
|
|
28
|
+
* // Compare
|
|
29
|
+
* areCoordinatesEqual(coords, { lat: -37.8137, lng: 144.9632 }); // true (within tolerance)
|
|
30
|
+
*
|
|
31
|
+
* // Generate Google Maps URL
|
|
32
|
+
* getGoogleMapsUrl(coords); // "https://www.google.com/maps/search/?api=1&query=-37.8136,144.9631"
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Coordinate interface for latitude and longitude
|
|
38
|
+
*/
|
|
39
|
+
export interface Coordinates {
|
|
40
|
+
lat: number;
|
|
41
|
+
lng: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Format coordinates as a string with 6 decimal places
|
|
46
|
+
*
|
|
47
|
+
* @param coords - Coordinate object with lat and lng
|
|
48
|
+
* @returns Formatted string "lat, lng" or "N/A" if invalid
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* formatCoordinates({ lat: -37.8136, lng: 144.9631 });
|
|
53
|
+
* // "-37.813600, 144.963100"
|
|
54
|
+
*
|
|
55
|
+
* formatCoordinates(undefined);
|
|
56
|
+
* // "N/A"
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function formatCoordinates(coords?: Coordinates): string {
|
|
60
|
+
if (!coords || typeof coords.lat !== 'number' || typeof coords.lng !== 'number') {
|
|
61
|
+
return 'N/A';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!isFinite(coords.lat) || !isFinite(coords.lng)) {
|
|
65
|
+
return 'N/A';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return `${coords.lat.toFixed(6)}, ${coords.lng.toFixed(6)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if coordinates are valid
|
|
73
|
+
*
|
|
74
|
+
* @param coords - Coordinate object to validate
|
|
75
|
+
* @returns true if coordinates are valid, false otherwise
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* hasValidCoordinates({ lat: -37.8136, lng: 144.9631 }); // true
|
|
80
|
+
* hasValidCoordinates({ lat: 91, lng: 0 }); // false (lat out of range)
|
|
81
|
+
* hasValidCoordinates(undefined); // false
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function hasValidCoordinates(coords?: { lat?: number; lng?: number }): boolean {
|
|
85
|
+
if (!coords) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { lat, lng } = coords;
|
|
90
|
+
|
|
91
|
+
if (typeof lat !== 'number' || typeof lng !== 'number') {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isFinite(lat) || !isFinite(lng)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Latitude must be between -90 and 90
|
|
100
|
+
if (lat < -90 || lat > 90) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Longitude must be between -180 and 180
|
|
105
|
+
if (lng < -180 || lng > 180) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if two coordinates are equal within a tolerance
|
|
114
|
+
*
|
|
115
|
+
* @param coords1 - First coordinate object
|
|
116
|
+
* @param coords2 - Second coordinate object
|
|
117
|
+
* @param tolerance - Tolerance in degrees (default: 0.0001° ≈ 11 meters)
|
|
118
|
+
* @returns true if coordinates are within tolerance, false otherwise
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const coords1 = { lat: -37.8136, lng: 144.9631 };
|
|
123
|
+
* const coords2 = { lat: -37.8137, lng: 144.9632 };
|
|
124
|
+
* areCoordinatesEqual(coords1, coords2); // true (within default tolerance)
|
|
125
|
+
* areCoordinatesEqual(coords1, coords2, 0.00001); // false (stricter tolerance)
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function areCoordinatesEqual(
|
|
129
|
+
coords1: Coordinates | null | undefined,
|
|
130
|
+
coords2: Coordinates | null | undefined,
|
|
131
|
+
tolerance: number = 0.0001
|
|
132
|
+
): boolean {
|
|
133
|
+
// Both null/undefined are considered equal
|
|
134
|
+
if (!coords1 && !coords2) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// One null/undefined and one not are not equal
|
|
139
|
+
if (!coords1 || !coords2) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Validate both coordinates
|
|
144
|
+
if (!hasValidCoordinates(coords1) || !hasValidCoordinates(coords2)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check if within tolerance (with small epsilon for floating point precision)
|
|
149
|
+
const epsilon = 1e-10;
|
|
150
|
+
const latDiff = Math.abs(coords1.lat - coords2.lat);
|
|
151
|
+
const lngDiff = Math.abs(coords1.lng - coords2.lng);
|
|
152
|
+
|
|
153
|
+
return latDiff <= tolerance + epsilon && lngDiff <= tolerance + epsilon;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generate a Google Maps search URL for coordinates
|
|
158
|
+
*
|
|
159
|
+
* @param coords - Coordinate object with lat and lng
|
|
160
|
+
* @returns Google Maps search URL or empty string if invalid
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* getGoogleMapsUrl({ lat: -37.8136, lng: 144.9631 });
|
|
165
|
+
* // "https://www.google.com/maps/search/?api=1&query=-37.8136,144.9631"
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export function getGoogleMapsUrl(coords?: Coordinates): string {
|
|
169
|
+
if (!coords || !hasValidCoordinates(coords)) {
|
|
170
|
+
return '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return `https://www.google.com/maps/search/?api=1&query=${coords.lat},${coords.lng}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
@@ -102,7 +102,7 @@ export const createSecureDataAccess = (
|
|
|
102
102
|
'cake_meal', 'cake_mealtype', 'pace_person', 'pace_member',
|
|
103
103
|
// SECURITY: Phase 3A additions - medical and personal data
|
|
104
104
|
'medi_profile', 'medi_condition', 'medi_diet', 'medi_action_plan', 'medi_profile_versions',
|
|
105
|
-
'pace_consent', 'pace_contact', '
|
|
105
|
+
'pace_consent', 'pace_contact', 'pace_identification', 'pace_identification_type', 'pace_qualification',
|
|
106
106
|
'form_responses', 'form_response_values', 'forms',
|
|
107
107
|
// SECURITY: Phase 3B additions - remaining critical tables
|
|
108
108
|
'invoice', 'line_item', 'credit_balance', 'payment_method',
|
|
@@ -153,6 +153,66 @@ async function generateFileHash(file: File): Promise<string> {
|
|
|
153
153
|
return `sha256:${hashHex}`;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Ensure a folder exists in the storage bucket
|
|
158
|
+
* In Supabase storage, folders are created automatically when files are uploaded,
|
|
159
|
+
* but this function explicitly creates the folder structure by uploading a placeholder
|
|
160
|
+
* file if the folder doesn't exist
|
|
161
|
+
* @param supabase - Supabase client instance
|
|
162
|
+
* @param folderPath - Folder path to ensure exists (e.g., 'orgId/folder')
|
|
163
|
+
* @param bucketName - Bucket name
|
|
164
|
+
* @returns True if folder exists or was created, false on error
|
|
165
|
+
*/
|
|
166
|
+
async function ensureFolderExists(
|
|
167
|
+
supabase: SupabaseClient,
|
|
168
|
+
folderPath: string,
|
|
169
|
+
bucketName: string
|
|
170
|
+
): Promise<boolean> {
|
|
171
|
+
try {
|
|
172
|
+
// Check if folder exists by trying to list it
|
|
173
|
+
const { data, error } = await supabase.storage
|
|
174
|
+
.from(bucketName)
|
|
175
|
+
.list(folderPath, {
|
|
176
|
+
limit: 1
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// If listing succeeds (even with empty data), the folder exists
|
|
180
|
+
if (!error) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// If we get a "not found" error, the folder doesn't exist yet
|
|
185
|
+
// Create it by uploading a placeholder file
|
|
186
|
+
// Supabase storage doesn't support empty folders, so we create a .keep file
|
|
187
|
+
const placeholderPath = `${folderPath}/.keep`;
|
|
188
|
+
const placeholderBlob = new Blob([''], { type: 'text/plain' });
|
|
189
|
+
const placeholderFile = new File([placeholderBlob], '.keep', { type: 'text/plain' });
|
|
190
|
+
|
|
191
|
+
const { error: uploadError } = await supabase.storage
|
|
192
|
+
.from(bucketName)
|
|
193
|
+
.upload(placeholderPath, placeholderFile, {
|
|
194
|
+
cacheControl: '3600',
|
|
195
|
+
upsert: true, // Use upsert to avoid errors if file already exists
|
|
196
|
+
contentType: 'text/plain'
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (uploadError) {
|
|
200
|
+
// If we can't create the placeholder, log it but don't fail
|
|
201
|
+
// The folder will be created automatically when we upload the actual file
|
|
202
|
+
log.debug(`Could not create folder placeholder (will be created on upload): ${uploadError.message}`);
|
|
203
|
+
return true; // Still return true - folder will be created on actual file upload
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Folder structure now exists
|
|
207
|
+
return true;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// If there's an exception, log it but proceed anyway
|
|
210
|
+
// The folder structure will be created when we upload the actual file
|
|
211
|
+
log.debug(`Folder creation exception (will be created on upload): ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
212
|
+
return true; // Return true to proceed - folder will be created on actual file upload
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
156
216
|
/**
|
|
157
217
|
* Upload a file to Supabase storage with app segregation
|
|
158
218
|
*/
|
|
@@ -175,13 +235,21 @@ export async function uploadFile(
|
|
|
175
235
|
const uniqueFileName = generateUniqueFileName(file.name);
|
|
176
236
|
const filePath = generateFilePath(options, uniqueFileName);
|
|
177
237
|
|
|
238
|
+
// Extract folder path from file path (everything except the filename)
|
|
239
|
+
const folderPath = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
240
|
+
|
|
178
241
|
// Extract metadata
|
|
179
242
|
const metadata = await extractFileMetadata(file, options, 'current-user'); // TODO: Get actual user ID
|
|
180
243
|
|
|
181
244
|
// Select bucket based on isPublic flag
|
|
182
245
|
const bucketName = getBucketName(options.isPublic || false);
|
|
183
246
|
|
|
247
|
+
// Ensure folder exists (Supabase creates folders automatically on upload,
|
|
248
|
+
// but we verify the path is accessible)
|
|
249
|
+
await ensureFolderExists(supabase, folderPath, bucketName);
|
|
250
|
+
|
|
184
251
|
// Upload file to Supabase
|
|
252
|
+
// Note: Supabase will automatically create the folder structure if it doesn't exist
|
|
185
253
|
const { data, error } = await supabase.storage
|
|
186
254
|
.from(bucketName)
|
|
187
255
|
.upload(filePath, file, {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Timezone Utilities Exports
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Timezone
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
formatInTimeZone,
|
|
10
|
+
getTimezoneAbbreviation,
|
|
11
|
+
formatTimeInTimeZone,
|
|
12
|
+
getUserTimeZone,
|
|
13
|
+
toZonedTime,
|
|
14
|
+
fromZonedTime,
|
|
15
|
+
roundToNearestMinutes,
|
|
16
|
+
getTimeZoneDifference
|
|
17
|
+
} from './timezone';
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Timezone Utilities Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Timezone/__tests__
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for timezone utility functions.
|
|
8
|
+
* Tests cover all major functionality, edge cases, and error handling.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
12
|
+
import {
|
|
13
|
+
formatInTimeZone,
|
|
14
|
+
getTimezoneAbbreviation,
|
|
15
|
+
formatTimeInTimeZone,
|
|
16
|
+
getUserTimeZone,
|
|
17
|
+
toZonedTime,
|
|
18
|
+
fromZonedTime,
|
|
19
|
+
roundToNearestMinutes,
|
|
20
|
+
getTimeZoneDifference
|
|
21
|
+
} from './timezone';
|
|
22
|
+
|
|
23
|
+
describe('Timezone Utilities', () => {
|
|
24
|
+
describe('formatInTimeZone', () => {
|
|
25
|
+
it('formats a Date object in a specific timezone', () => {
|
|
26
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
27
|
+
const result = formatInTimeZone(date, 'America/New_York', 'MMM dd, yyyy HH:mm');
|
|
28
|
+
expect(result).toMatch(/Jan 15, 2024/);
|
|
29
|
+
expect(result).toMatch(/\d{2}:\d{2}/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('formats an ISO string in a specific timezone', () => {
|
|
33
|
+
const result = formatInTimeZone('2024-01-15T10:00:00Z', 'America/New_York', 'MMM dd, yyyy');
|
|
34
|
+
expect(result).toMatch(/Jan 15, 2024/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('formats a timestamp in a specific timezone', () => {
|
|
38
|
+
const timestamp = new Date('2024-01-15T10:00:00Z').getTime();
|
|
39
|
+
const result = formatInTimeZone(timestamp, 'America/New_York', 'yyyy-MM-dd');
|
|
40
|
+
expect(result).toBe('2024-01-15');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns "Invalid date" for invalid date input', () => {
|
|
44
|
+
const result = formatInTimeZone(new Date('invalid'), 'America/New_York', 'MMM dd, yyyy');
|
|
45
|
+
expect(result).toBe('Invalid date');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns "Invalid date" for invalid timezone', () => {
|
|
49
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
50
|
+
const result = formatInTimeZone(date, '', 'MMM dd, yyyy');
|
|
51
|
+
expect(result).toBe('Invalid date');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns "Invalid date" for non-string timezone', () => {
|
|
55
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
56
|
+
// @ts-expect-error - Testing invalid input
|
|
57
|
+
const result = formatInTimeZone(date, null, 'MMM dd, yyyy');
|
|
58
|
+
expect(result).toBe('Invalid date');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('handles different timezones correctly', () => {
|
|
62
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
63
|
+
const ny = formatInTimeZone(date, 'America/New_York', 'HH:mm');
|
|
64
|
+
const la = formatInTimeZone(date, 'America/Los_Angeles', 'HH:mm');
|
|
65
|
+
expect(ny).not.toBe(la);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('getTimezoneAbbreviation', () => {
|
|
70
|
+
it('returns timezone abbreviation for valid date and timezone', () => {
|
|
71
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
72
|
+
const result = getTimezoneAbbreviation(date, 'America/New_York');
|
|
73
|
+
expect(typeof result).toBe('string');
|
|
74
|
+
expect(result.length).toBeGreaterThan(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns timezone name as fallback when abbreviation unavailable', () => {
|
|
78
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
79
|
+
const result = getTimezoneAbbreviation(date, 'America/New_York');
|
|
80
|
+
expect(result).toBeTruthy();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns timezone string for invalid date', () => {
|
|
84
|
+
const result = getTimezoneAbbreviation(new Date('invalid'), 'America/New_York');
|
|
85
|
+
expect(result).toBe('America/New_York');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns "UTC" for empty timezone', () => {
|
|
89
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
90
|
+
const result = getTimezoneAbbreviation(date, '');
|
|
91
|
+
expect(result).toBe('UTC');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('handles invalid timezone gracefully', () => {
|
|
95
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
96
|
+
const result = getTimezoneAbbreviation(date, 'Invalid/Timezone');
|
|
97
|
+
expect(typeof result).toBe('string');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('formatTimeInTimeZone', () => {
|
|
102
|
+
it('formats time only in specific timezone', () => {
|
|
103
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
104
|
+
const result = formatTimeInTimeZone(date, 'America/New_York');
|
|
105
|
+
expect(result).toMatch(/^\d{2}:\d{2}$/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('formats ISO string time in timezone', () => {
|
|
109
|
+
const result = formatTimeInTimeZone('2024-01-15T10:00:00Z', 'America/New_York');
|
|
110
|
+
expect(result).toMatch(/^\d{2}:\d{2}$/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns "Invalid date" for invalid input', () => {
|
|
114
|
+
const result = formatTimeInTimeZone(new Date('invalid'), 'America/New_York');
|
|
115
|
+
expect(result).toBe('Invalid date');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('getUserTimeZone', () => {
|
|
120
|
+
it('returns IANA timezone string', () => {
|
|
121
|
+
const result = getUserTimeZone();
|
|
122
|
+
expect(typeof result).toBe('string');
|
|
123
|
+
expect(result.length).toBeGreaterThan(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns "UTC" as fallback when Intl is unavailable', () => {
|
|
127
|
+
const originalIntl = global.Intl;
|
|
128
|
+
// @ts-expect-error - Testing fallback behavior
|
|
129
|
+
global.Intl = undefined;
|
|
130
|
+
const result = getUserTimeZone();
|
|
131
|
+
expect(result).toBe('UTC');
|
|
132
|
+
global.Intl = originalIntl;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('returns "UTC" on error', () => {
|
|
136
|
+
const originalDateTimeFormat = Intl.DateTimeFormat;
|
|
137
|
+
// @ts-expect-error - Testing error handling
|
|
138
|
+
Intl.DateTimeFormat = () => {
|
|
139
|
+
throw new Error('Test error');
|
|
140
|
+
};
|
|
141
|
+
const result = getUserTimeZone();
|
|
142
|
+
expect(result).toBe('UTC');
|
|
143
|
+
Intl.DateTimeFormat = originalDateTimeFormat;
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('toZonedTime', () => {
|
|
148
|
+
it('converts UTC date to timezone local time', () => {
|
|
149
|
+
const utcDate = new Date('2024-01-15T10:00:00Z');
|
|
150
|
+
const result = toZonedTime(utcDate, 'America/New_York');
|
|
151
|
+
expect(result).toBeInstanceOf(Date);
|
|
152
|
+
expect(result.getTime()).not.toBe(utcDate.getTime());
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns original date for invalid timezone', () => {
|
|
156
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
157
|
+
const result = toZonedTime(date, '');
|
|
158
|
+
expect(result).toBe(date);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('returns original date for invalid date', () => {
|
|
162
|
+
const invalidDate = new Date('invalid');
|
|
163
|
+
const result = toZonedTime(invalidDate, 'America/New_York');
|
|
164
|
+
expect(result).toBe(invalidDate);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('handles different timezones correctly', () => {
|
|
168
|
+
const utcDate = new Date('2024-01-15T10:00:00Z');
|
|
169
|
+
const ny = toZonedTime(utcDate, 'America/New_York');
|
|
170
|
+
const la = toZonedTime(utcDate, 'America/Los_Angeles');
|
|
171
|
+
expect(ny.getTime()).not.toBe(la.getTime());
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns original date on error', () => {
|
|
175
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
176
|
+
// @ts-expect-error - Testing error handling
|
|
177
|
+
const result = toZonedTime(date, null);
|
|
178
|
+
expect(result).toBe(date);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('fromZonedTime', () => {
|
|
183
|
+
it('converts local time in timezone to UTC', () => {
|
|
184
|
+
const localDate = new Date(2024, 0, 15, 10, 0); // Jan 15, 2024 10:00 AM local
|
|
185
|
+
const result = fromZonedTime(localDate, 'America/New_York');
|
|
186
|
+
expect(result).toBeInstanceOf(Date);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('returns original date for invalid timezone', () => {
|
|
190
|
+
const date = new Date(2024, 0, 15, 10, 0);
|
|
191
|
+
const result = fromZonedTime(date, '');
|
|
192
|
+
expect(result).toBe(date);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('returns original date for invalid date', () => {
|
|
196
|
+
const invalidDate = new Date('invalid');
|
|
197
|
+
const result = fromZonedTime(invalidDate, 'America/New_York');
|
|
198
|
+
expect(result).toBe(invalidDate);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('handles round-trip conversion', () => {
|
|
202
|
+
const utcDate = new Date('2024-01-15T10:00:00Z');
|
|
203
|
+
const zoned = toZonedTime(utcDate, 'America/New_York');
|
|
204
|
+
const backToUtc = fromZonedTime(zoned, 'America/New_York');
|
|
205
|
+
// Should be close to original (within a few milliseconds due to DST handling)
|
|
206
|
+
expect(Math.abs(backToUtc.getTime() - utcDate.getTime())).toBeLessThan(1000);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('returns original date on error', () => {
|
|
210
|
+
const date = new Date(2024, 0, 15, 10, 0);
|
|
211
|
+
// @ts-expect-error - Testing error handling
|
|
212
|
+
const result = fromZonedTime(date, null);
|
|
213
|
+
expect(result).toBe(date);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('roundToNearestMinutes', () => {
|
|
218
|
+
it('rounds to nearest 5 minutes by default', () => {
|
|
219
|
+
const date = new Date('2024-01-15T10:23:00Z');
|
|
220
|
+
const result = roundToNearestMinutes(date);
|
|
221
|
+
expect(result.getMinutes() % 5).toBe(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('rounds to nearest specified minutes', () => {
|
|
225
|
+
const date = new Date('2024-01-15T10:23:00Z');
|
|
226
|
+
const result = roundToNearestMinutes(date, 10);
|
|
227
|
+
expect(result.getMinutes() % 10).toBe(0);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('rounds up when closer to next interval', () => {
|
|
231
|
+
const date = new Date('2024-01-15T10:28:00Z');
|
|
232
|
+
const result = roundToNearestMinutes(date, 5);
|
|
233
|
+
expect(result.getMinutes()).toBe(30);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('rounds down when closer to previous interval', () => {
|
|
237
|
+
const date = new Date('2024-01-15T10:22:00Z');
|
|
238
|
+
const result = roundToNearestMinutes(date, 5);
|
|
239
|
+
expect(result.getMinutes()).toBe(20);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('returns original date for invalid date', () => {
|
|
243
|
+
const invalidDate = new Date('invalid');
|
|
244
|
+
const result = roundToNearestMinutes(invalidDate, 5);
|
|
245
|
+
expect(result).toBe(invalidDate);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('returns original date for invalid minutesStep', () => {
|
|
249
|
+
const date = new Date('2024-01-15T10:23:00Z');
|
|
250
|
+
const result = roundToNearestMinutes(date, 0);
|
|
251
|
+
expect(date).toBe(date);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('returns original date for negative minutesStep', () => {
|
|
255
|
+
const date = new Date('2024-01-15T10:23:00Z');
|
|
256
|
+
const result = roundToNearestMinutes(date, -5);
|
|
257
|
+
expect(result).toBe(date);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('returns original date for non-integer minutesStep', () => {
|
|
261
|
+
const date = new Date('2024-01-15T10:23:00Z');
|
|
262
|
+
const result = roundToNearestMinutes(date, 5.5);
|
|
263
|
+
expect(result).toBe(date);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('handles edge case of exact minute', () => {
|
|
267
|
+
const date = new Date('2024-01-15T10:25:00Z');
|
|
268
|
+
const result = roundToNearestMinutes(date, 5);
|
|
269
|
+
expect(result.getMinutes()).toBe(25);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('getTimeZoneDifference', () => {
|
|
274
|
+
it('calculates difference between two timezones', () => {
|
|
275
|
+
const result = getTimeZoneDifference('America/New_York', 'America/Los_Angeles');
|
|
276
|
+
expect(typeof result).toBe('number');
|
|
277
|
+
// Los Angeles is typically 3 hours behind New York
|
|
278
|
+
expect(result).toBeLessThan(0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('returns 0 for same timezone', () => {
|
|
282
|
+
const result = getTimeZoneDifference('America/New_York', 'America/New_York');
|
|
283
|
+
expect(result).toBe(0);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('returns 0 for invalid timezones', () => {
|
|
287
|
+
const result = getTimeZoneDifference('', 'America/New_York');
|
|
288
|
+
expect(result).toBe(0);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('returns 0 for non-string timezones', () => {
|
|
292
|
+
// @ts-expect-error - Testing invalid input
|
|
293
|
+
const result = getTimeZoneDifference(null, 'America/New_York');
|
|
294
|
+
expect(result).toBe(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('handles DST correctly', () => {
|
|
298
|
+
// Test in summer (EDT vs PDT = 3 hours)
|
|
299
|
+
const summerDate = new Date('2024-07-15T10:00:00Z');
|
|
300
|
+
const result = getTimeZoneDifference('America/New_York', 'America/Los_Angeles');
|
|
301
|
+
// Should be negative (LA behind NY)
|
|
302
|
+
expect(result).toBeLessThan(0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('returns 0 on error', () => {
|
|
306
|
+
// This test verifies error handling path
|
|
307
|
+
const result = getTimeZoneDifference('Invalid/Timezone1', 'Invalid/Timezone2');
|
|
308
|
+
expect(result).toBe(0);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('Edge Cases', () => {
|
|
313
|
+
it('handles null and undefined inputs gracefully', () => {
|
|
314
|
+
// @ts-expect-error - Testing edge cases
|
|
315
|
+
expect(formatInTimeZone(null, 'America/New_York', 'yyyy-MM-dd')).toBe('Invalid date');
|
|
316
|
+
// @ts-expect-error - Testing edge cases
|
|
317
|
+
expect(formatInTimeZone(undefined, 'America/New_York', 'yyyy-MM-dd')).toBe('Invalid date');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('handles extreme dates', () => {
|
|
321
|
+
const farFuture = new Date('2099-12-31T23:59:59Z');
|
|
322
|
+
const result = formatInTimeZone(farFuture, 'America/New_York', 'yyyy-MM-dd');
|
|
323
|
+
expect(result).toMatch(/2099-12-31/);
|
|
324
|
+
|
|
325
|
+
const farPast = new Date('1900-01-01T00:00:00Z');
|
|
326
|
+
const result2 = formatInTimeZone(farPast, 'America/New_York', 'yyyy-MM-dd');
|
|
327
|
+
// Timezone conversion may shift the date to previous day due to timezone offset
|
|
328
|
+
// Accept either 1900-01-01 or 1899-12-31 depending on timezone offset
|
|
329
|
+
expect(result2).toMatch(/1899-12-31|1900-01-01/);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('handles various timezone formats', () => {
|
|
333
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
334
|
+
const timezones = [
|
|
335
|
+
'America/New_York',
|
|
336
|
+
'Europe/London',
|
|
337
|
+
'Asia/Tokyo',
|
|
338
|
+
'Australia/Sydney',
|
|
339
|
+
'UTC'
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
timezones.forEach(tz => {
|
|
343
|
+
const result = formatInTimeZone(date, tz, 'yyyy-MM-dd');
|
|
344
|
+
expect(result).toBe('2024-01-15');
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|