@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.
Files changed (319) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +60 -1
  3. package/core-usage-manifest.json +312 -0
  4. package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
  5. package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
  6. package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-DIzEzwKl.d.ts} +23 -10
  7. package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
  8. package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
  9. package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
  10. package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
  11. package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
  12. package/dist/{chunk-W22JP75J.js → chunk-DAGICKHT.js} +9 -7
  13. package/dist/chunk-DAGICKHT.js.map +1 -0
  14. package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
  15. package/dist/chunk-FXFJRTKI.js.map +1 -0
  16. package/dist/{chunk-CSOFYHAG.js → chunk-GRIQLQ52.js} +374 -60
  17. package/dist/chunk-GRIQLQ52.js.map +1 -0
  18. package/dist/{chunk-NQPMQGS2.js → chunk-HDCUMOOI.js} +497 -399
  19. package/dist/chunk-HDCUMOOI.js.map +1 -0
  20. package/dist/chunk-HESYZWZW.js +388 -0
  21. package/dist/chunk-HESYZWZW.js.map +1 -0
  22. package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
  23. package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
  24. package/dist/{chunk-PWAHJW4G.js → chunk-OALXJH4Y.js} +86 -33
  25. package/dist/chunk-OALXJH4Y.js.map +1 -0
  26. package/dist/{chunk-MI7HBHN3.js → chunk-TC7D3CR3.js} +89 -9
  27. package/dist/chunk-TC7D3CR3.js.map +1 -0
  28. package/dist/chunk-THRPYOFK.js +215 -0
  29. package/dist/chunk-THRPYOFK.js.map +1 -0
  30. package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
  31. package/dist/chunk-U6WNSFX5.js.map +1 -0
  32. package/dist/{chunk-UHNYIBXL.js → chunk-UQWSHFVX.js} +1 -1
  33. package/dist/chunk-UQWSHFVX.js.map +1 -0
  34. package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
  35. package/dist/components.d.ts +182 -6
  36. package/dist/components.js +157 -11
  37. package/dist/components.js.map +1 -1
  38. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  39. package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
  40. package/dist/{file-reference-D06mEEWW.d.ts → file-reference-PRTSLxKx.d.ts} +10 -1
  41. package/dist/hooks.d.ts +52 -15
  42. package/dist/hooks.js +12 -22
  43. package/dist/hooks.js.map +1 -1
  44. package/dist/index.d.ts +12 -12
  45. package/dist/index.js +82 -18
  46. package/dist/index.js.map +1 -1
  47. package/dist/providers.d.ts +1 -1
  48. package/dist/providers.js +3 -1
  49. package/dist/rbac/index.d.ts +206 -15
  50. package/dist/rbac/index.js +28 -6
  51. package/dist/timezone-_pgH8qrY.d.ts +530 -0
  52. package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
  53. package/dist/types.d.ts +2 -2
  54. package/dist/types.js +1 -1
  55. package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +114 -3
  56. package/dist/utils.d.ts +110 -152
  57. package/dist/utils.js +128 -138
  58. package/dist/utils.js.map +1 -1
  59. package/docs/api/README.md +60 -1
  60. package/docs/api/classes/ColumnFactory.md +1 -1
  61. package/docs/api/classes/ErrorBoundary.md +1 -1
  62. package/docs/api/classes/InvalidScopeError.md +1 -1
  63. package/docs/api/classes/Logger.md +178 -0
  64. package/docs/api/classes/MissingUserContextError.md +1 -1
  65. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  66. package/docs/api/classes/PermissionDeniedError.md +1 -1
  67. package/docs/api/classes/RBACAuditManager.md +2 -2
  68. package/docs/api/classes/RBACCache.md +1 -1
  69. package/docs/api/classes/RBACEngine.md +2 -2
  70. package/docs/api/classes/RBACError.md +1 -1
  71. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  72. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  73. package/docs/api/classes/StorageUtils.md +1 -1
  74. package/docs/api/enums/FileCategory.md +1 -1
  75. package/docs/api/enums/LogLevel.md +54 -0
  76. package/docs/api/enums/RBACErrorCode.md +1 -1
  77. package/docs/api/enums/RPCFunction.md +1 -1
  78. package/docs/api/interfaces/AggregateConfig.md +1 -1
  79. package/docs/api/interfaces/BadgeProps.md +1 -1
  80. package/docs/api/interfaces/ButtonProps.md +1 -1
  81. package/docs/api/interfaces/CalendarProps.md +18 -2
  82. package/docs/api/interfaces/CardProps.md +1 -1
  83. package/docs/api/interfaces/ColorPalette.md +1 -1
  84. package/docs/api/interfaces/ColorShade.md +1 -1
  85. package/docs/api/interfaces/ComplianceResult.md +30 -0
  86. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  87. package/docs/api/interfaces/DataRecord.md +1 -1
  88. package/docs/api/interfaces/DataTableAction.md +1 -1
  89. package/docs/api/interfaces/DataTableColumn.md +1 -1
  90. package/docs/api/interfaces/DataTableProps.md +1 -1
  91. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  92. package/docs/api/interfaces/DatabaseComplianceResult.md +85 -0
  93. package/docs/api/interfaces/DatabaseIssue.md +41 -0
  94. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  95. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/EventAppRoleData.md +6 -6
  97. package/docs/api/interfaces/ExportColumn.md +1 -1
  98. package/docs/api/interfaces/ExportOptions.md +1 -1
  99. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  100. package/docs/api/interfaces/FileMetadata.md +1 -1
  101. package/docs/api/interfaces/FileReference.md +1 -1
  102. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  103. package/docs/api/interfaces/FileUploadOptions.md +48 -8
  104. package/docs/api/interfaces/FileUploadProps.md +46 -13
  105. package/docs/api/interfaces/FooterProps.md +1 -1
  106. package/docs/api/interfaces/FormFieldProps.md +1 -1
  107. package/docs/api/interfaces/FormProps.md +1 -1
  108. package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
  109. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  110. package/docs/api/interfaces/InputProps.md +1 -1
  111. package/docs/api/interfaces/LabelProps.md +1 -1
  112. package/docs/api/interfaces/LoggerConfig.md +62 -0
  113. package/docs/api/interfaces/LoginFormProps.md +1 -1
  114. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  115. package/docs/api/interfaces/NavigationContextType.md +1 -1
  116. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  117. package/docs/api/interfaces/NavigationItem.md +1 -1
  118. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  119. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  120. package/docs/api/interfaces/Organisation.md +1 -1
  121. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  122. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  123. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  124. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  125. package/docs/api/interfaces/PaceAppLayoutProps.md +36 -23
  126. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  127. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  128. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  129. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  130. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  131. package/docs/api/interfaces/PaletteData.md +1 -1
  132. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  133. package/docs/api/interfaces/ProgressProps.md +1 -1
  134. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  135. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  136. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  137. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  138. package/docs/api/interfaces/QuickFix.md +52 -0
  139. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  140. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  141. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  142. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  143. package/docs/api/interfaces/RBACConfig.md +4 -4
  144. package/docs/api/interfaces/RBACContext.md +1 -1
  145. package/docs/api/interfaces/RBACLogger.md +1 -1
  146. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  148. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  149. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  150. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  151. package/docs/api/interfaces/RBACResult.md +1 -1
  152. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  153. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  154. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  155. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  156. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  157. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  158. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  159. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  160. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  161. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  162. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  163. package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
  164. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  165. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  166. package/docs/api/interfaces/RoleManagementResult.md +5 -5
  167. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  168. package/docs/api/interfaces/RouteConfig.md +1 -1
  169. package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
  170. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  171. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  172. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  173. package/docs/api/interfaces/SetupIssue.md +41 -0
  174. package/docs/api/interfaces/StorageConfig.md +1 -1
  175. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  176. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  177. package/docs/api/interfaces/StorageListOptions.md +1 -1
  178. package/docs/api/interfaces/StorageListResult.md +1 -1
  179. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  180. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  181. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  182. package/docs/api/interfaces/StyleImport.md +1 -1
  183. package/docs/api/interfaces/SwitchProps.md +1 -1
  184. package/docs/api/interfaces/TabsContentProps.md +1 -1
  185. package/docs/api/interfaces/TabsListProps.md +1 -1
  186. package/docs/api/interfaces/TabsProps.md +1 -1
  187. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  188. package/docs/api/interfaces/TextareaProps.md +1 -1
  189. package/docs/api/interfaces/ToastActionElement.md +1 -1
  190. package/docs/api/interfaces/ToastProps.md +1 -1
  191. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  192. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  193. package/docs/api/interfaces/UseFormDialogOptions.md +62 -0
  194. package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
  195. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  196. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  198. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  199. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  200. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  201. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  202. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  203. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  204. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  205. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  206. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  207. package/docs/api/interfaces/UserEventAccess.md +1 -1
  208. package/docs/api/interfaces/UserMenuProps.md +1 -1
  209. package/docs/api/interfaces/UserProfile.md +1 -1
  210. package/docs/api/modules.md +746 -50
  211. package/docs/api-reference/components.md +26 -12
  212. package/docs/api-reference/hooks.md +111 -0
  213. package/docs/api-reference/rpc-functions.md +1 -1
  214. package/docs/api-reference/utilities.md +184 -0
  215. package/docs/getting-started/installation-guide.md +75 -16
  216. package/docs/getting-started/quick-start.md +61 -11
  217. package/docs/implementation-guides/authentication.md +88 -12
  218. package/docs/implementation-guides/file-reference-system.md +26 -3
  219. package/docs/implementation-guides/file-upload-storage.md +30 -1
  220. package/docs/rbac/README.md +1 -0
  221. package/docs/rbac/compliance/compliance-guide.md +544 -0
  222. package/docs/rbac/getting-started.md +158 -33
  223. package/docs/standards/pace-core-compliance.md +432 -0
  224. package/eslint-config-pace-core.cjs +93 -0
  225. package/package.json +15 -3
  226. package/scripts/analyze-bundle.js +232 -0
  227. package/scripts/build-css.js +56 -0
  228. package/scripts/build-docs-incremental.js +1015 -0
  229. package/scripts/check-pace-core-compliance.cjs +2353 -0
  230. package/scripts/check-pace-core-compliance.js +512 -0
  231. package/scripts/generate-docs.js +157 -0
  232. package/scripts/setup-build-cache.js +73 -0
  233. package/scripts/utils/command-runner.js +131 -0
  234. package/scripts/utils/env.js +33 -0
  235. package/scripts/utils/index.js +10 -0
  236. package/scripts/utils/logger.js +88 -0
  237. package/scripts/utils/path-helpers.js +37 -0
  238. package/scripts/validate-formats.js +133 -0
  239. package/scripts/validate-master.js +155 -0
  240. package/scripts/validate-pre-publish.js +140 -0
  241. package/scripts/validate-theme.js +142 -0
  242. package/src/components/Calendar/Calendar.tsx +8 -1
  243. package/src/components/Card/Card.tsx +47 -8
  244. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
  245. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
  246. package/src/components/DatePickerWithTimezone/README.md +135 -0
  247. package/src/components/DatePickerWithTimezone/index.ts +10 -0
  248. package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
  249. package/src/components/DateTimeField/DateTimeField.tsx +232 -0
  250. package/src/components/DateTimeField/README.md +148 -0
  251. package/src/components/DateTimeField/index.ts +10 -0
  252. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  253. package/src/components/FileUpload/FileUpload.tsx +10 -1
  254. package/src/components/Header/Header.test.tsx +47 -18
  255. package/src/components/Header/Header.tsx +22 -7
  256. package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
  257. package/src/components/PaceAppLayout/README.md +9 -0
  258. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
  259. package/src/components/ProtectedRoute/ProtectedRoute.tsx +146 -5
  260. package/src/components/index.ts +8 -0
  261. package/src/eslint-rules/pace-core-compliance.cjs +406 -0
  262. package/src/eslint-rules/pace-core-compliance.js +640 -0
  263. package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
  264. package/src/hooks/index.ts +5 -0
  265. package/src/hooks/useFileReference.test.ts +2 -0
  266. package/src/hooks/useFormDialog.ts +147 -0
  267. package/src/hooks/usePreventTabReload.ts +106 -0
  268. package/src/hooks/useSecureDataAccess.ts +2 -2
  269. package/src/index.ts +27 -0
  270. package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
  271. package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
  272. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  273. package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
  274. package/src/rbac/compliance/database-validator.ts +165 -0
  275. package/src/rbac/compliance/index.ts +38 -0
  276. package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
  277. package/src/rbac/compliance/runtime-compliance.ts +77 -0
  278. package/src/rbac/compliance/setup-validator.ts +131 -0
  279. package/src/rbac/components/PagePermissionGuard.tsx +8 -64
  280. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
  281. package/src/rbac/docs/event-based-apps.md +285 -0
  282. package/src/rbac/errors.ts +11 -0
  283. package/src/rbac/hooks/useRoleManagement.ts +292 -12
  284. package/src/rbac/index.ts +30 -0
  285. package/src/services/OrganisationService.ts +4 -0
  286. package/src/styles/core.css +5 -5
  287. package/src/types/database.generated.ts +63 -9
  288. package/src/types/file-reference.ts +9 -0
  289. package/src/utils/__tests__/timezone.test.ts +345 -0
  290. package/src/utils/file-reference/__tests__/file-reference.test.ts +60 -4
  291. package/src/utils/file-reference/index.ts +13 -2
  292. package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
  293. package/src/utils/formatting/formatting.ts +179 -0
  294. package/src/utils/index.ts +27 -1
  295. package/src/utils/location/index.ts +16 -0
  296. package/src/utils/location/location.test.ts +286 -0
  297. package/src/utils/location/location.ts +175 -0
  298. package/src/utils/security/secureDataAccess.ts +1 -1
  299. package/src/utils/storage/helpers.ts +68 -0
  300. package/src/utils/timezone/index.ts +17 -0
  301. package/src/utils/timezone/timezone.test.ts +349 -0
  302. package/src/utils/timezone/timezone.ts +281 -0
  303. package/dist/chunk-CSOFYHAG.js.map +0 -1
  304. package/dist/chunk-FUEYYMX5.js.map +0 -1
  305. package/dist/chunk-HKIT6O7W.js +0 -198
  306. package/dist/chunk-HKIT6O7W.js.map +0 -1
  307. package/dist/chunk-KUEN3HFB.js +0 -94
  308. package/dist/chunk-KUEN3HFB.js.map +0 -1
  309. package/dist/chunk-M7W4CP3M.js.map +0 -1
  310. package/dist/chunk-MI7HBHN3.js.map +0 -1
  311. package/dist/chunk-NQPMQGS2.js.map +0 -1
  312. package/dist/chunk-PWAHJW4G.js.map +0 -1
  313. package/dist/chunk-UHNYIBXL.js.map +0 -1
  314. package/dist/chunk-W22JP75J.js.map +0 -1
  315. package/dist/formatting-5wETwiGF.d.ts +0 -162
  316. /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
  317. /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
  318. /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
  319. /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', 'pace_id_documents', 'pace_qualifications',
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
+