@fleetbase/fleetops-engine 0.6.71 → 0.6.72

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 (378) hide show
  1. package/RELEASE.md +15 -18
  2. package/addon/components/admin/tracking-page-settings.hbs +56 -0
  3. package/addon/components/admin/tracking-page-settings.js +67 -0
  4. package/addon/components/device/form.hbs +2 -2
  5. package/addon/components/driver/details.hbs +1 -1
  6. package/addon/components/driver/form.hbs +1 -1
  7. package/addon/components/equipment/form.hbs +1 -1
  8. package/addon/components/layout/fleet-ops-sidebar.js +11 -3
  9. package/addon/components/maintenance/form.hbs +3 -3
  10. package/addon/components/map/google-live-map.js +2 -2
  11. package/addon/components/map/leaflet-live-map.js +45 -32
  12. package/addon/components/orchestrator/phase-builder.js +2 -1
  13. package/addon/components/orchestrator-workbench.hbs +7 -1
  14. package/addon/components/orchestrator-workbench.js +16 -2
  15. package/addon/components/order/form/route.hbs +1 -1
  16. package/addon/components/order/form/route.js +13 -1
  17. package/addon/components/service-rate/details.hbs +1 -1
  18. package/addon/components/service-rate/form.hbs +1 -1
  19. package/addon/components/tracking-page/full-view.hbs +276 -0
  20. package/addon/components/tracking-page/full-view.js +474 -0
  21. package/addon/components/tracking-page/locale-selector.js +30 -0
  22. package/addon/components/tracking-page/map.hbs +44 -0
  23. package/addon/components/tracking-page/map.js +328 -0
  24. package/addon/components/tracking-page/order-list.hbs +35 -0
  25. package/addon/components/tracking-page/public-view.hbs +60 -0
  26. package/addon/components/tracking-page/settings-preview.hbs +21 -0
  27. package/addon/components/tracking-page/settings-preview.js +23 -0
  28. package/addon/components/tracking-page/verify-panel.hbs +109 -0
  29. package/addon/components/tracking-page.hbs +142 -0
  30. package/addon/components/tracking-page.js +564 -0
  31. package/addon/components/trailer/form.hbs +2 -2
  32. package/addon/components/vehicle/form.hbs +3 -3
  33. package/addon/components/warranty/form.hbs +2 -2
  34. package/addon/components/widget/live-fleet.js +54 -6
  35. package/addon/components/widget/proof-of-delivery.hbs +49 -0
  36. package/addon/components/widget/proof-of-delivery.js +62 -0
  37. package/addon/components/work-order/form.hbs +3 -3
  38. package/addon/controllers/operations/orders/index/details.js +19 -3
  39. package/addon/controllers/operations/scheduler/index.js +45 -14
  40. package/addon/controllers/settings/index.js +6 -0
  41. package/addon/controllers/settings/map.js +75 -0
  42. package/addon/controllers/settings/orchestrator.js +8 -6
  43. package/addon/controllers/settings/tracking-page.js +212 -0
  44. package/addon/extension.js +65 -1
  45. package/addon/helpers/parcel-box-image.js +16 -0
  46. package/addon/routes/operations/orders/index/new.js +5 -4
  47. package/addon/routes/operations/scheduler/index.js +1 -1
  48. package/addon/routes/settings/tracking-page.js +31 -0
  49. package/addon/routes.js +1 -0
  50. package/addon/services/location.js +69 -21
  51. package/addon/services/map-settings.js +28 -0
  52. package/addon/services/order-allocation.js +1 -1
  53. package/addon/styles/fleetops-engine.css +1391 -0
  54. package/addon/templates/settings/map.hbs +46 -0
  55. package/addon/templates/settings/tracking-page.hbs +318 -0
  56. package/addon/utils/identity-cell-resource.js +2 -56
  57. package/addon/utils/map-default-view.js +132 -0
  58. package/addon/utils/placeholder-images.js +4 -69
  59. package/addon/utils/resource-descriptors/assets.js +2 -57
  60. package/addon/utils/resource-descriptors/helpers.js +23 -122
  61. package/addon/utils/resource-descriptors/operations.js +2 -61
  62. package/addon/utils/resource-descriptors/people.js +3 -142
  63. package/addon/utils/resource-descriptors/shared.js +18 -0
  64. package/addon/utils/tracking-live-source.js +123 -0
  65. package/addon/utils/tracking-page-theme.js +123 -0
  66. package/app/components/admin/tracking-page-settings.js +1 -0
  67. package/app/components/tracking-page/full-view.js +1 -0
  68. package/app/components/tracking-page/locale-selector.js +1 -0
  69. package/app/components/tracking-page/map.js +1 -0
  70. package/app/components/tracking-page/order-list.js +1 -0
  71. package/app/components/tracking-page/public-view.js +1 -0
  72. package/app/components/tracking-page/settings-preview.js +1 -0
  73. package/app/components/tracking-page/verify-panel.js +1 -0
  74. package/app/components/tracking-page.js +1 -0
  75. package/app/components/widget/proof-of-delivery.js +1 -0
  76. package/app/controllers/settings/tracking-page.js +1 -0
  77. package/app/helpers/parcel-box-image.js +1 -0
  78. package/app/routes/settings/tracking-page.js +1 -0
  79. package/app/templates/settings/tracking-page.js +1 -0
  80. package/app/utils/map-default-view.js +1 -0
  81. package/app/utils/tracking-live-source.js +1 -0
  82. package/app/utils/tracking-page-theme.js +1 -0
  83. package/composer.json +2 -2
  84. package/extension.json +1 -1
  85. package/package.json +4 -4
  86. package/scripts/pest-bootstrap.php +4 -0
  87. package/server/config/fleetops.php +4 -0
  88. package/server/migrations/2026_10_08_000001_create_tracking_sessions_tables.php +51 -0
  89. package/server/migrations/2026_10_10_000001_add_trashed_index_to_device_events.php +40 -0
  90. package/server/migrations/2026_10_10_000002_add_trashed_index_to_positions.php +40 -0
  91. package/server/migrations/2026_10_12_000001_add_deleted_at_to_tracking_sessions_tables.php +32 -0
  92. package/server/resources/views/labels/default.php +2 -2
  93. package/server/resources/views/labels/entity-label.php +2 -2
  94. package/server/resources/views/labels/waypoint-label.php +2 -2
  95. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  96. package/server/src/Console/Commands/ProcessOperationalAlerts.php +1 -0
  97. package/server/src/Console/Commands/PruneTelematicsData.php +101 -26
  98. package/server/src/Events/OrderChanged.php +257 -0
  99. package/server/src/Events/OrderDriverUnassigned.php +68 -0
  100. package/server/src/Exceptions/OrderEditLockedException.php +32 -0
  101. package/server/src/Expansions/CompanyExpansion.php +3 -2
  102. package/server/src/Expansions/UserExpansion.php +12 -10
  103. package/server/src/Http/Controllers/Api/v1/CustomerController.php +108 -18
  104. package/server/src/Http/Controllers/Api/v1/DriverController.php +95 -10
  105. package/server/src/Http/Controllers/Api/v1/EntityController.php +66 -0
  106. package/server/src/Http/Controllers/Api/v1/FleetController.php +10 -4
  107. package/server/src/Http/Controllers/Api/v1/OrderController.php +139 -36
  108. package/server/src/Http/Controllers/Api/v1/PayloadController.php +76 -8
  109. package/server/src/Http/Controllers/Api/v1/PlaceController.php +3 -3
  110. package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +6 -32
  111. package/server/src/Http/Controllers/Api/v1/VehicleController.php +5 -0
  112. package/server/src/Http/Controllers/Internal/v1/ContactController.php +3 -0
  113. package/server/src/Http/Controllers/Internal/v1/DriverController.php +10 -0
  114. package/server/src/Http/Controllers/Internal/v1/FleetController.php +16 -2
  115. package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +13 -8
  116. package/server/src/Http/Controllers/Internal/v1/IssueController.php +2 -0
  117. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +4 -2
  118. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +93 -11
  119. package/server/src/Http/Controllers/Internal/v1/OrderController.php +84 -24
  120. package/server/src/Http/Controllers/Internal/v1/PositionController.php +22 -0
  121. package/server/src/Http/Controllers/Internal/v1/ProofController.php +55 -4
  122. package/server/src/Http/Controllers/Internal/v1/SettingController.php +303 -10
  123. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +1 -0
  124. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +1 -0
  125. package/server/src/Http/Controllers/Public/TrackingController.php +518 -0
  126. package/server/src/Http/Filter/ContactFilter.php +1 -1
  127. package/server/src/Http/Filter/InspectionFormFilter.php +23 -0
  128. package/server/src/Http/Filter/InspectionSubmissionFilter.php +23 -0
  129. package/server/src/Http/Filter/MaintenanceFilter.php +23 -0
  130. package/server/src/Http/Filter/OrderFilter.php +39 -4
  131. package/server/src/Http/Filter/ProofFilter.php +18 -0
  132. package/server/src/Http/Filter/RouteFilter.php +18 -0
  133. package/server/src/Http/Filter/TelematicFilter.php +23 -0
  134. package/server/src/Http/Filter/WarrantyFilter.php +23 -0
  135. package/server/src/Http/Requests/CancelOrderRequest.php +2 -1
  136. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +14 -1
  137. package/server/src/Http/Resources/v1/Index/Customer.php +18 -0
  138. package/server/src/Http/Resources/v1/Index/Order.php +11 -2
  139. package/server/src/Http/Resources/v1/PublicOrderTracking.php +153 -0
  140. package/server/src/Http/Resources/v1/TrackingNumber.php +8 -34
  141. package/server/src/Http/Resources/v1/Zone.php +31 -23
  142. package/server/src/Jobs/FinalizeApiOrderCreation.php +3 -1
  143. package/server/src/Jobs/ProcessAllocationJob.php +16 -6
  144. package/server/src/Jobs/PublishTrackingUpdate.php +50 -0
  145. package/server/src/Listeners/HandleGeofenceEntered.php +31 -15
  146. package/server/src/Listeners/HandleOrderCanceled.php +9 -1
  147. package/server/src/Listeners/HandleUserRemovedFromCompany.php +2 -1
  148. package/server/src/Listeners/PublishTrackingUpdates.php +27 -0
  149. package/server/src/Mail/CustomerCredentialsMail.php +4 -2
  150. package/server/src/Models/AssetConnection.php +3 -0
  151. package/server/src/Models/Contact.php +3 -0
  152. package/server/src/Models/Device.php +28 -0
  153. package/server/src/Models/DeviceEvent.php +7 -0
  154. package/server/src/Models/Driver.php +20 -7
  155. package/server/src/Models/Entity.php +12 -4
  156. package/server/src/Models/Fleet.php +11 -32
  157. package/server/src/Models/FleetDriver.php +3 -0
  158. package/server/src/Models/FleetVehicle.php +3 -0
  159. package/server/src/Models/FuelReport.php +5 -13
  160. package/server/src/Models/Issue.php +4 -16
  161. package/server/src/Models/Order.php +200 -9
  162. package/server/src/Models/Payload.php +227 -35
  163. package/server/src/Models/Place.php +7 -1
  164. package/server/src/Models/ServiceArea.php +7 -0
  165. package/server/src/Models/ServiceRate.php +7 -17
  166. package/server/src/Models/TrackingNumber.php +101 -2
  167. package/server/src/Models/TrackingSession.php +33 -0
  168. package/server/src/Models/TrackingSessionGrant.php +22 -0
  169. package/server/src/Models/TrackingStatus.php +6 -0
  170. package/server/src/Models/Vehicle.php +21 -2
  171. package/server/src/Models/Vendor.php +7 -0
  172. package/server/src/Models/Waypoint.php +13 -4
  173. package/server/src/Notifications/OrderAssigned.php +2 -1
  174. package/server/src/Notifications/OrderCanceled.php +7 -1
  175. package/server/src/Notifications/OrderChanged.php +87 -0
  176. package/server/src/Notifications/OrderCompleted.php +2 -1
  177. package/server/src/Notifications/OrderDispatchFailed.php +2 -1
  178. package/server/src/Notifications/OrderDispatched.php +2 -1
  179. package/server/src/Notifications/OrderFailed.php +2 -1
  180. package/server/src/Notifications/OrderUnassigned.php +66 -0
  181. package/server/src/Notifications/WaypointCompleted.php +2 -1
  182. package/server/src/Observers/CompanyUserObserver.php +8 -7
  183. package/server/src/Observers/DriverObserver.php +6 -1
  184. package/server/src/Observers/OrderObserver.php +141 -1
  185. package/server/src/Observers/TrackingNumberObserver.php +18 -6
  186. package/server/src/Observers/UserObserver.php +2 -1
  187. package/server/src/Observers/VehicleObserver.php +8 -3
  188. package/server/src/Orchestration/Engines/RouteSequencingEngine.php +21 -3
  189. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +38 -8
  190. package/server/src/Orchestration/OrchestrationEngineRegistry.php +61 -1
  191. package/server/src/Orchestration/Support/OrchestratorSettings.php +61 -0
  192. package/server/src/Orchestration/Support/RouteMetrics.php +165 -0
  193. package/server/src/Providers/EventServiceProvider.php +11 -0
  194. package/server/src/Providers/FleetOpsServiceProvider.php +13 -0
  195. package/server/src/Support/ApiCacheThrottle.php +76 -0
  196. package/server/src/Support/LiveOrderQuery.php +3 -1
  197. package/server/src/Support/OrderEdit.php +365 -0
  198. package/server/src/Support/OrderEditPolicy.php +292 -0
  199. package/server/src/Support/ProfileAccountManager.php +54 -1
  200. package/server/src/Support/ResolvesOrderServiceStops.php +5 -0
  201. package/server/src/Support/SocketChannels.php +346 -0
  202. package/server/src/Support/Telematics/TelematicService.php +7 -2
  203. package/server/src/Support/Telematics/Telemetry/Inbox.php +8 -1
  204. package/server/src/Support/TrackingChannel.php +55 -0
  205. package/server/src/Support/TrackingCode.php +242 -0
  206. package/server/src/Support/TrackingPage/TrackingAccess.php +356 -0
  207. package/server/src/Support/TrackingPage/TrackingPageConfig.php +264 -0
  208. package/server/src/Support/TrackingPage/TrackingRecipient.php +111 -0
  209. package/server/src/Support/TrackingPage/TrackingResolver.php +201 -0
  210. package/server/src/Support/TrackingPage/TrackingTarget.php +40 -0
  211. package/server/src/Support/TrackingPage/TrackingView.php +543 -0
  212. package/server/src/Support/TrackingPublisher.php +261 -0
  213. package/server/src/Support/TrackingScope.php +182 -0
  214. package/server/src/Support/TrackingUpdate.php +402 -0
  215. package/server/src/Support/Utils.php +15 -3
  216. package/server/src/Traits/HasTrackingNumber.php +2 -0
  217. package/server/src/routes.php +39 -7
  218. package/server/tests/ApiCustomerControllerContractsTest.php +54 -7
  219. package/server/tests/ApiDriverPasswordContractsTest.php +25 -21
  220. package/server/tests/ApiEntityControllerContractsTest.php +84 -0
  221. package/server/tests/ApiOrderControllerContractsTest.php +245 -0
  222. package/server/tests/ApiPayloadControllerContractsTest.php +121 -0
  223. package/server/tests/ApiPlaceControllerContractsTest.php +1 -1
  224. package/server/tests/ApiTrackingNumberControllerContractsTest.php +6 -9
  225. package/server/tests/CompactResourceSerializationTest.php +23 -59
  226. package/server/tests/ControllerFilterContractsTest.php +1 -1
  227. package/server/tests/CustomerContactIdentitySafetyTest.php +1 -1
  228. package/server/tests/EventContractsTest.php +26 -60
  229. package/server/tests/ExpansionContractsTest.php +15 -2
  230. package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +29 -3
  231. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +23 -0
  232. package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +16 -16
  233. package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +41 -14
  234. package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +57 -22
  235. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +23 -9
  236. package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +171 -1
  237. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
  238. package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +7 -5
  239. package/server/tests/Feature/Http/Internal/PositionControllerReplayChannelTest.php +135 -0
  240. package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +3 -3
  241. package/server/tests/Feature/Http/Internal/ProofControllerRecentTest.php +170 -0
  242. package/server/tests/Feature/Http/Public/TrackingControllerTest.php +495 -0
  243. package/server/tests/IndexCustomerPhotoTest.php +151 -0
  244. package/server/tests/ModelAccessorContractsTest.php +4 -3
  245. package/server/tests/NotificationAndMailContractsTest.php +5 -8
  246. package/server/tests/ObserverContractsTest.php +72 -10
  247. package/server/tests/OrderActivityFlowRegressionTest.php +3 -2
  248. package/server/tests/OrderFilterExecutionTest.php +167 -2
  249. package/server/tests/ProcessAllocationJobContractsTest.php +20 -0
  250. package/server/tests/PublicOrderTrackingResourceTest.php +221 -0
  251. package/server/tests/QueueJobContractsTest.php +2 -1
  252. package/server/tests/RequestContractsTest.php +1 -1
  253. package/server/tests/RouteRegistrationExecutionTest.php +26 -0
  254. package/server/tests/RouteSequencingEngineTest.php +17 -0
  255. package/server/tests/SettingControllerContractsTest.php +110 -0
  256. package/server/tests/Support/TrackingPageDatabase.php +200 -0
  257. package/server/tests/TenantScopingTest.php +206 -0
  258. package/server/tests/TrackingPageSettingsTest.php +343 -0
  259. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +51 -0
  260. package/server/tests/Unit/Http/Filter/ContactFilterAddressTest.php +61 -0
  261. package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +30 -0
  262. package/server/tests/Unit/Jobs/FinalizeApiOrderCreationDispatchGuardTest.php +266 -0
  263. package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +182 -22
  264. package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +5 -0
  265. package/server/tests/Unit/Models/DriverAssignVehicleTest.php +81 -0
  266. package/server/tests/Unit/Models/FleetTest.php +4 -3
  267. package/server/tests/Unit/Models/OrderCancelReasonTest.php +143 -0
  268. package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +60 -0
  269. package/server/tests/Unit/Models/OrderPayloadRouteTest.php +4 -2
  270. package/server/tests/Unit/Models/PayloadTest.php +18 -4
  271. package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +2 -2
  272. package/server/tests/Unit/Models/TrackingNumberTest.php +67 -6
  273. package/server/tests/Unit/Models/VehicleTest.php +16 -2
  274. package/server/tests/Unit/Models/WaypointTest.php +4 -3
  275. package/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +59 -0
  276. package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +3 -3
  277. package/server/tests/Unit/Observers/DriverDeletionCleanupTest.php +158 -0
  278. package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +20 -0
  279. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +3 -3
  280. package/server/tests/Unit/Observers/OrderAssignmentObserverTest.php +100 -0
  281. package/server/tests/Unit/Observers/OrderDeleteCascadeTest.php +213 -0
  282. package/server/tests/Unit/Observers/VehicleObserverDeleteTest.php +67 -0
  283. package/server/tests/Unit/Orchestration/EngineSelectionAndRouteMetricsTest.php +336 -0
  284. package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +8 -1
  285. package/server/tests/Unit/Support/ApiCacheDependenciesTest.php +347 -0
  286. package/server/tests/Unit/Support/OrderEditPolicyTest.php +156 -0
  287. package/server/tests/Unit/Support/OrderOperationsTest.php +514 -0
  288. package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +4 -3
  289. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +141 -0
  290. package/server/tests/Unit/Support/QuerySeamsTest.php +5 -1
  291. package/server/tests/Unit/Support/SocketChannelsTest.php +396 -0
  292. package/server/tests/Unit/Support/TrackingChannelTest.php +232 -0
  293. package/server/tests/Unit/Support/TrackingCodeTest.php +186 -0
  294. package/server/tests/Unit/Support/TrackingPage/TrackingAccessTest.php +200 -0
  295. package/server/tests/Unit/Support/TrackingPage/TrackingRecipientTest.php +51 -0
  296. package/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +181 -0
  297. package/server/tests/Unit/Support/TrackingPage/TrackingViewTest.php +479 -0
  298. package/server/tests/Unit/Support/TrackingPublisherTest.php +425 -0
  299. package/server/tests/Unit/Support/TrackingUpdateTest.php +363 -0
  300. package/server/tests/Unit/Support/UtilsAdditionalTest.php +9 -0
  301. package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +25 -0
  302. package/server/tests/VehicleResourceContractsTest.php +46 -0
  303. package/server/tests/VroomOrchestrationEngineTest.php +34 -1
  304. package/server/tests/ZoneGeofenceSettingsResourceTest.php +121 -0
  305. package/translations/ar-ae.yml +274 -0
  306. package/translations/bg-bg.yaml +274 -0
  307. package/translations/en-us.yaml +287 -0
  308. package/translations/es-pa.yaml +274 -0
  309. package/translations/fr-fr.yaml +274 -0
  310. package/translations/mn-mn.yaml +274 -0
  311. package/translations/pt-br.yaml +274 -0
  312. package/translations/ru-ru.yaml +276 -2
  313. package/translations/uk-ua.yaml +275 -1
  314. package/translations/vi-vn.yaml +274 -0
  315. package/addon/components/cell/contact-identity.hbs +0 -1
  316. package/addon/components/cell/customer-identity.hbs +0 -1
  317. package/addon/components/cell/driver-identity.hbs +0 -1
  318. package/addon/components/cell/fleet-identity.hbs +0 -1
  319. package/addon/components/cell/order-identity.hbs +0 -1
  320. package/addon/components/cell/place-identity.hbs +0 -1
  321. package/addon/components/cell/vehicle-identity.hbs +0 -1
  322. package/addon/components/cell/vendor-identity.hbs +0 -1
  323. package/addon/components/contact/pill.hbs +0 -24
  324. package/addon/components/contact/summary.hbs +0 -1
  325. package/addon/components/customer/pill.hbs +0 -24
  326. package/addon/components/customer/summary.hbs +0 -1
  327. package/addon/components/driver/pill.hbs +0 -24
  328. package/addon/components/driver/summary.hbs +0 -1
  329. package/addon/components/fleet/pill.hbs +0 -24
  330. package/addon/components/fleet/summary.hbs +0 -1
  331. package/addon/components/order/pill.hbs +0 -24
  332. package/addon/components/order/summary.hbs +0 -1
  333. package/addon/components/place/pill.hbs +0 -24
  334. package/addon/components/place/summary.hbs +0 -1
  335. package/addon/components/select-option/contact.hbs +0 -12
  336. package/addon/components/select-option/customer.hbs +0 -12
  337. package/addon/components/select-option/driver.hbs +0 -12
  338. package/addon/components/select-option/fleet.hbs +0 -12
  339. package/addon/components/select-option/order.hbs +0 -12
  340. package/addon/components/select-option/place.hbs +0 -12
  341. package/addon/components/select-option/vehicle.hbs +0 -12
  342. package/addon/components/select-option/vendor.hbs +0 -12
  343. package/addon/components/vehicle/pill.hbs +0 -24
  344. package/addon/components/vehicle/summary.hbs +0 -1
  345. package/addon/components/vendor/pill.hbs +0 -24
  346. package/addon/components/vendor/summary.hbs +0 -1
  347. package/app/components/cell/contact-identity.js +0 -1
  348. package/app/components/cell/customer-identity.js +0 -1
  349. package/app/components/cell/driver-identity.js +0 -1
  350. package/app/components/cell/fleet-identity.js +0 -1
  351. package/app/components/cell/order-identity.js +0 -1
  352. package/app/components/cell/place-identity.js +0 -1
  353. package/app/components/cell/vehicle-identity.js +0 -1
  354. package/app/components/cell/vendor-identity.js +0 -1
  355. package/app/components/contact/pill.js +0 -1
  356. package/app/components/contact/summary.js +0 -1
  357. package/app/components/customer/pill.js +0 -1
  358. package/app/components/customer/summary.js +0 -1
  359. package/app/components/driver/pill.js +0 -1
  360. package/app/components/driver/summary.js +0 -1
  361. package/app/components/fleet/pill.js +0 -1
  362. package/app/components/fleet/summary.js +0 -1
  363. package/app/components/order/pill.js +0 -1
  364. package/app/components/order/summary.js +0 -1
  365. package/app/components/place/pill.js +0 -1
  366. package/app/components/place/summary.js +0 -1
  367. package/app/components/select-option/contact.js +0 -1
  368. package/app/components/select-option/customer.js +0 -1
  369. package/app/components/select-option/driver.js +0 -1
  370. package/app/components/select-option/fleet.js +0 -1
  371. package/app/components/select-option/order.js +0 -1
  372. package/app/components/select-option/place.js +0 -1
  373. package/app/components/select-option/vehicle.js +0 -1
  374. package/app/components/select-option/vendor.js +0 -1
  375. package/app/components/vehicle/pill.js +0 -1
  376. package/app/components/vehicle/summary.js +0 -1
  377. package/app/components/vendor/pill.js +0 -1
  378. package/app/components/vendor/summary.js +0 -1
@@ -81,6 +81,52 @@
81
81
  {{/if}}
82
82
  </ContentPanel>
83
83
 
84
+ {{! ─── Default Map View Panel ──────────────────────────────────────── }}
85
+ <ContentPanel @title={{t "settings.map.default-view"}} @open={{true}} @wrapperClass="bordered-classic">
86
+ {{#if this.settingsLoaded}}
87
+ <p class="mb-4 text-sm text-gray-500 dark:text-gray-400">{{t "settings.map.default-view-help-text"}}</p>
88
+
89
+ <InputGroup @name={{t "settings.map.default-center"}} @helpText={{t "settings.map.default-center-help-text"}}>
90
+ {{#each (array this.defaultCenterInputKey) as |inputKey|}}
91
+ <CoordinatesInput
92
+ data-input-key={{inputKey}}
93
+ @value={{this.defaultMapCenterPoint}}
94
+ @onChange={{this.setDefaultMapCenter}}
95
+ @onUpdatedFromMap={{this.setDefaultMapCenter}}
96
+ @disabled={{cannot "fleet-ops update map-settings"}}
97
+ />
98
+ {{/each}}
99
+ {{#if this.hasDefaultMapCenter}}
100
+ <Button
101
+ @type="link"
102
+ @size="xs"
103
+ @icon="times"
104
+ @text={{t "settings.map.clear-default-center"}}
105
+ @onClick={{this.clearDefaultMapCenter}}
106
+ @permission="fleet-ops update map-settings"
107
+ />
108
+ {{/if}}
109
+ </InputGroup>
110
+
111
+ <InputGroup @name={{t "settings.map.default-zoom"}} @helpText={{t "settings.map.default-zoom-help-text"}}>
112
+ <Input @type="number" @value={{this.defaultMapZoom}} min="1" max="20" step="1" placeholder="12" class="w-full form-input" />
113
+ </InputGroup>
114
+
115
+ <InputGroup>
116
+ <Toggle
117
+ @label={{t "settings.map.fit-to-vehicles"}}
118
+ @isToggled={{this.fitMapToVehicles}}
119
+ @onToggle={{fn (mut this.fitMapToVehicles)}}
120
+ @helpText={{t "settings.map.fit-to-vehicles-help-text"}}
121
+ />
122
+ </InputGroup>
123
+ {{else}}
124
+ <div class="flex items-center justify-center py-8">
125
+ <Spinner />
126
+ </div>
127
+ {{/if}}
128
+ </ContentPanel>
129
+
84
130
  {{! Extension point for third-party panels }}
85
131
  <RegistryYield @registry="fleet-ops:template:settings:map" as |RegistryComponent|>
86
132
  <RegistryComponent @controller={{this}} />
@@ -0,0 +1,318 @@
1
+ <Layout::Section::Header @title={{t "tracking-page.settings.title"}}>
2
+ <Button
3
+ @type="primary"
4
+ @size="sm"
5
+ @icon="save"
6
+ @text={{t "common.save-changes"}}
7
+ @onClick={{perform this.saveSettings}}
8
+ @disabled={{or this.saveSettings.isRunning this.loadFailed}}
9
+ @isLoading={{this.saveSettings.isRunning}}
10
+ />
11
+ </Layout::Section::Header>
12
+
13
+ <Layout::Section::Body class="overflow-y-scroll-i h-full">
14
+ <div class="container mx-auto">
15
+ {{#if this.loadFailed}}
16
+ <div class="max-w-3xl my-10 mx-auto rounded-lg border border-gray-200 dark:border-gray-700 p-6 text-sm">
17
+ <p class="font-semibold mb-1">{{t "tracking-page.settings.load-failed"}}</p>
18
+ <p class="text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.load-failed-help"}}</p>
19
+ </div>
20
+ {{else if this.config}}
21
+ <div class="my-10 mx-auto max-w-6xl grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
22
+ <div class="lg:col-span-2 space-y-6">
23
+
24
+ {{! ── Pages ─────────────────────────────────────────────── }}
25
+ <ContentPanel @title={{t "tracking-page.settings.pages"}} @open={{true}} @wrapperClass="bordered-classic">
26
+ <div class="space-y-5">
27
+ <div class="flex items-start justify-between gap-4">
28
+ <div>
29
+ <div class="text-sm font-semibold">{{t "tracking-page.settings.org-page-enabled"}}</div>
30
+ <div class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.org-page-enabled-help"}}</div>
31
+ </div>
32
+ <Toggle @isToggled={{this.config.org_page.enabled}} @onToggle={{fn this.update "org_page.enabled"}} />
33
+ </div>
34
+
35
+ <div>
36
+ <label for="tracking-page-slug" class="block text-sm font-semibold mb-1">{{t "tracking-page.settings.slug"}}</label>
37
+ <div class="flex items-stretch rounded-md border border-gray-300 dark:border-gray-700 overflow-hidden">
38
+ <span class="flex items-center px-3 text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 border-r border-gray-300 dark:border-gray-700 whitespace-nowrap">{{this.orgPageBase}}</span>
39
+ <input
40
+ id="tracking-page-slug"
41
+ type="text"
42
+ class="form-input border-0 rounded-none flex-1 min-w-0"
43
+ value={{this.config.org_page.slug}}
44
+ placeholder={{t "tracking-page.settings.placeholder-slug"}}
45
+ autocomplete="off"
46
+ spellcheck="false"
47
+ aria-describedby="tracking-page-slug-status"
48
+ {{on "input" this.updateSlug}}
49
+ />
50
+ </div>
51
+ <div id="tracking-page-slug-status" class="mt-1 text-xs {{if this.slugIsValid 'text-green-700 dark:text-green-400' 'text-red-700 dark:text-red-400'}}" aria-live="polite">
52
+ {{#if this.validateSlug.isRunning}}
53
+ <span class="text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.slug-checking"}}</span>
54
+ {{else if this.slugValidation}}
55
+ {{this.slugValidation.message}}
56
+ {{/if}}
57
+ </div>
58
+ <div class="mt-2 flex flex-wrap items-center gap-2">
59
+ <ClickToCopy class="px-3 py-1.5 border rounded-md text-xs bg-gray-100 dark:bg-gray-700 border-gray-200 dark:border-gray-800" @value={{this.orgPageUrl}}>{{this.orgPageUrl}}</ClickToCopy>
60
+ {{#if this.config.org_page.enabled}}
61
+ <a href={{this.orgPageUrl}} target="_blank" rel="noopener noreferrer" class="text-xs font-semibold text-blue-600 dark:text-blue-400">{{t "tracking-page.settings.open-page"}}</a>
62
+ {{/if}}
63
+ </div>
64
+ </div>
65
+
66
+ <div class="flex items-start justify-between gap-4">
67
+ <div>
68
+ <div class="text-sm font-semibold">{{t "tracking-page.settings.generic-page-allowed"}}</div>
69
+ <div class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.generic-page-allowed-help" url=this.genericPageUrl}}</div>
70
+ </div>
71
+ <Toggle @isToggled={{this.config.generic_page.allowed}} @onToggle={{fn this.update "generic_page.allowed"}} />
72
+ </div>
73
+
74
+ <InputGroup @name={{t "tracking-page.settings.links-target"}} @helpText={{t "tracking-page.settings.links-target-help"}}>
75
+ <Select
76
+ class="w-full"
77
+ @placeholder={{t "tracking-page.settings.placeholder-select"}}
78
+ @value={{this.config.links.target}}
79
+ @options={{this.linkTargetOptions}}
80
+ @optionLabel="label"
81
+ @optionValue="value"
82
+ @onSelect={{fn this.selectOption "links.target"}}
83
+ />
84
+ </InputGroup>
85
+ </div>
86
+ </ContentPanel>
87
+
88
+ {{! ── Branding ──────────────────────────────────────────── }}
89
+ <ContentPanel @title={{t "tracking-page.settings.branding"}} @open={{true}} @wrapperClass="bordered-classic">
90
+ <div class="space-y-5">
91
+ <div>
92
+ <label for="tracking-page-display-name" class="block text-sm font-semibold mb-1">{{t "tracking-page.settings.display-name"}}</label>
93
+ <input
94
+ id="tracking-page-display-name"
95
+ type="text"
96
+ class="form-input w-full"
97
+ value={{this.config.branding.display_name}}
98
+ placeholder={{this.companyName}}
99
+ {{on "input" (fn this.updateFromEvent "branding.display_name")}}
100
+ />
101
+ <p class="mt-1 text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.logo-help"}}</p>
102
+ </div>
103
+
104
+ <div>
105
+ <label for="tracking-page-accent" class="block text-sm font-semibold mb-1">{{t "tracking-page.settings.accent"}}</label>
106
+ <div class="flex items-center gap-3">
107
+ <input
108
+ id="tracking-page-accent"
109
+ type="color"
110
+ class="h-10 w-14 rounded-md border border-gray-300 dark:border-gray-700 bg-transparent cursor-pointer"
111
+ value={{this.accent}}
112
+ {{on "input" (fn this.updateFromEvent "branding.accent")}}
113
+ />
114
+ <input
115
+ type="text"
116
+ class="form-input w-32 font-mono uppercase"
117
+ aria-label={{t "tracking-page.settings.accent-hex"}}
118
+ value={{this.config.branding.accent}}
119
+ placeholder="#1F5FA8"
120
+ {{on "change" (fn this.updateFromEvent "branding.accent")}}
121
+ />
122
+ <span class="text-xs {{if this.accentReadable 'text-green-700 dark:text-green-400' 'text-red-700 dark:text-red-400'}}">
123
+ {{#if this.accentReadable}}
124
+ {{t "tracking-page.settings.contrast-ok" ratio=this.accentContrast}}
125
+ {{else}}
126
+ {{t "tracking-page.settings.contrast-low" ratio=this.accentContrast}}
127
+ {{/if}}
128
+ </span>
129
+ </div>
130
+ <p class="mt-1 text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.accent-help"}}</p>
131
+ </div>
132
+
133
+ <div>
134
+ <label for="tracking-page-accent-dark" class="block text-sm font-semibold mb-1">{{t "tracking-page.settings.accent-dark"}}</label>
135
+ <input
136
+ id="tracking-page-accent-dark"
137
+ type="text"
138
+ class="form-input w-32 font-mono uppercase"
139
+ value={{this.config.branding.accent_dark}}
140
+ placeholder={{t "tracking-page.settings.accent-dark-auto"}}
141
+ {{on "change" (fn this.updateFromEvent "branding.accent_dark")}}
142
+ />
143
+ </div>
144
+
145
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
146
+ <div>
147
+ <label for="tracking-page-phone" class="block text-sm font-semibold mb-1">{{t "tracking-page.settings.support-phone"}}</label>
148
+ <input id="tracking-page-phone" type="tel" class="form-input w-full" value={{this.config.branding.support_phone}} placeholder={{t "tracking-page.settings.placeholder-phone"}} {{on "input" (fn this.updateFromEvent "branding.support_phone")}} />
149
+ </div>
150
+ <div>
151
+ <label for="tracking-page-email" class="block text-sm font-semibold mb-1">{{t "tracking-page.settings.support-email"}}</label>
152
+ <input id="tracking-page-email" type="email" class="form-input w-full" value={{this.config.branding.support_email}} placeholder={{t "tracking-page.settings.placeholder-email"}} {{on "input" (fn this.updateFromEvent "branding.support_email")}} />
153
+ </div>
154
+ <div>
155
+ <label for="tracking-page-website" class="block text-sm font-semibold mb-1">{{t "tracking-page.settings.website"}}</label>
156
+ <input id="tracking-page-website" type="url" class="form-input w-full" value={{this.config.branding.website}} placeholder={{t "tracking-page.settings.placeholder-website"}} {{on "input" (fn this.updateFromEvent "branding.website")}} />
157
+ </div>
158
+ </div>
159
+ <p class="-mt-3 text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.support-help"}}</p>
160
+
161
+ <div class="flex items-start justify-between gap-4">
162
+ <div>
163
+ <div class="text-sm font-semibold">{{t "tracking-page.settings.powered-by"}}</div>
164
+ <div class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.powered-by-help"}}</div>
165
+ </div>
166
+ <Toggle @isToggled={{this.config.branding.powered_by}} @onToggle={{fn this.update "branding.powered_by"}} />
167
+ </div>
168
+
169
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
170
+ <InputGroup @name={{t "tracking-page.settings.theme"}}>
171
+ <Select
172
+ class="w-full"
173
+ @placeholder={{t "tracking-page.settings.placeholder-select"}}
174
+ @value={{this.config.branding.theme}}
175
+ @options={{this.themeOptions}}
176
+ @optionLabel="label"
177
+ @optionValue="value"
178
+ @onSelect={{fn this.selectOption "branding.theme"}}
179
+ />
180
+ </InputGroup>
181
+ <InputGroup @name={{t "tracking-page.settings.default-language"}}>
182
+ <Select
183
+ class="w-full"
184
+ @placeholder={{t "tracking-page.settings.placeholder-select"}}
185
+ @value={{this.config.branding.default_locale}}
186
+ @options={{this.defaultLocaleOptions}}
187
+ @optionLabel="label"
188
+ @optionValue="value"
189
+ @onSelect={{this.setDefaultLocale}}
190
+ />
191
+ </InputGroup>
192
+ </div>
193
+
194
+ <fieldset>
195
+ <legend class="text-sm font-semibold mb-2">{{t "tracking-page.settings.languages"}}</legend>
196
+ <div class="grid grid-cols-2 md:grid-cols-5 gap-2">
197
+ {{#each this.localeOptions as |locale|}}
198
+ <label class="flex items-center gap-2 text-sm">
199
+ <input type="checkbox" checked={{locale.enabled}} disabled={{locale.isDefault}} {{on "change" (fn this.toggleLocale locale.code)}} />
200
+ <span>{{locale.label}}</span>
201
+ </label>
202
+ {{/each}}
203
+ </div>
204
+ </fieldset>
205
+ </div>
206
+ </ContentPanel>
207
+
208
+ {{! ── Access ────────────────────────────────────────────── }}
209
+ <ContentPanel @title={{t "tracking-page.settings.access"}} @open={{true}} @wrapperClass="bordered-classic">
210
+ <div class="space-y-5">
211
+ <div class="flex items-start justify-between gap-4">
212
+ <div>
213
+ <div class="text-sm font-semibold">{{t "tracking-page.settings.public-status"}}</div>
214
+ <div class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.public-status-help"}}</div>
215
+ </div>
216
+ <Toggle @isToggled={{this.config.access.public_status}} @onToggle={{fn this.update "access.public_status"}} />
217
+ </div>
218
+
219
+ <div class="flex items-start justify-between gap-4">
220
+ <div>
221
+ <div class="text-sm font-semibold">{{t "tracking-page.settings.channel-sms"}}</div>
222
+ {{#if this.smsAvailable}}
223
+ <div class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.channel-sms-help"}}</div>
224
+ {{else}}
225
+ <div class="text-xs text-red-700 dark:text-red-400">{{t "tracking-page.settings.channel-sms-unavailable"}}</div>
226
+ {{/if}}
227
+ </div>
228
+ <Toggle @isToggled={{and this.smsAvailable this.config.access.channels.sms}} @disabled={{not this.smsAvailable}} @onToggle={{fn this.update "access.channels.sms"}} />
229
+ </div>
230
+
231
+ <div class="flex items-start justify-between gap-4">
232
+ <div>
233
+ <div class="text-sm font-semibold">{{t "tracking-page.settings.channel-email"}}</div>
234
+ <div class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.channel-email-help"}}</div>
235
+ </div>
236
+ <Toggle @isToggled={{this.config.access.channels.email}} @onToggle={{fn this.update "access.channels.email"}} />
237
+ </div>
238
+
239
+ <div>
240
+ <label for="tracking-page-session-hours" class="block text-sm font-semibold mb-1">{{t "tracking-page.settings.session-hours"}}</label>
241
+ <input
242
+ id="tracking-page-session-hours"
243
+ type="number"
244
+ min="1"
245
+ max="168"
246
+ class="form-input w-32"
247
+ value={{this.config.access.session_hours}}
248
+ placeholder="24"
249
+ {{on "change" (fn this.updateNumber "access.session_hours")}}
250
+ />
251
+ <p class="mt-1 text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.session-hours-help"}}</p>
252
+ </div>
253
+
254
+ {{#if this.customerPortalInstalled}}
255
+ <div class="flex items-start justify-between gap-4">
256
+ <div>
257
+ <div class="text-sm font-semibold">{{t "tracking-page.settings.account-sign-in"}}</div>
258
+ <div class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.account-sign-in-help"}}</div>
259
+ </div>
260
+ <Toggle @isToggled={{this.config.access.account_sign_in}} @onToggle={{fn this.update "access.account_sign_in"}} />
261
+ </div>
262
+ <div class="flex items-start justify-between gap-4">
263
+ <div>
264
+ <div class="text-sm font-semibold">{{t "tracking-page.settings.account-upsell"}}</div>
265
+ <div class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.account-upsell-help"}}</div>
266
+ </div>
267
+ <Toggle @isToggled={{this.config.access.account_upsell}} @onToggle={{fn this.update "access.account_upsell"}} />
268
+ </div>
269
+ {{else}}
270
+ <p class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.customer-portal-missing"}}</p>
271
+ {{/if}}
272
+
273
+ <p class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.code-rules"}}</p>
274
+ </div>
275
+ </ContentPanel>
276
+
277
+ {{! ── What verified customers see ───────────────────────── }}
278
+ <ContentPanel @title={{t "tracking-page.settings.visibility"}} @open={{true}} @wrapperClass="bordered-classic">
279
+ <div class="space-y-4">
280
+ {{#each (array "map" "driver_name" "vehicle" "items" "item_prices" "pod_photo" "pod_signature" "instructions_edit" "report_problem") as |key|}}
281
+ <div class="flex items-start justify-between gap-4">
282
+ <div>
283
+ <div class="text-sm font-semibold">{{t (concat "tracking-page.settings.visibility-" key)}}</div>
284
+ <div class="text-xs text-gray-600 dark:text-gray-300">{{t (concat "tracking-page.settings.visibility-" key "-help")}}</div>
285
+ </div>
286
+ <Toggle @isToggled={{get this.config.visibility key}} @onToggle={{fn this.update (concat "visibility." key)}} />
287
+ </div>
288
+ {{/each}}
289
+
290
+ <InputGroup @name={{t "tracking-page.settings.driver-contact"}} @helpText={{t "tracking-page.settings.driver-contact-help"}}>
291
+ <Select
292
+ class="w-full"
293
+ @placeholder={{t "tracking-page.settings.placeholder-select"}}
294
+ @value={{this.config.visibility.driver_contact}}
295
+ @options={{this.driverContactOptions}}
296
+ @optionLabel="label"
297
+ @optionValue="value"
298
+ @onSelect={{fn this.selectOption "visibility.driver_contact"}}
299
+ />
300
+ </InputGroup>
301
+
302
+ <p class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.privacy-rules"}}</p>
303
+ </div>
304
+ </ContentPanel>
305
+ </div>
306
+
307
+ {{! ── Live preview ───────────────────────────────────────────── }}
308
+ <div class="lg:sticky lg:top-6 space-y-3">
309
+ <div class="text-sm font-semibold">{{t "tracking-page.settings.preview"}}</div>
310
+ <TrackingPage::SettingsPreview @themeStyle={{this.previewLightStyle}} @displayName={{this.displayName}} @poweredBy={{this.config.branding.powered_by}} />
311
+ <TrackingPage::SettingsPreview @themeStyle={{this.previewDarkStyle}} @displayName={{this.displayName}} @poweredBy={{this.config.branding.powered_by}} />
312
+ <p class="text-xs text-gray-600 dark:text-gray-300">{{t "tracking-page.settings.preview-help"}}</p>
313
+ </div>
314
+ </div>
315
+ {{/if}}
316
+ </div>
317
+ <Spacer @height="400px" />
318
+ </Layout::Section::Body>
@@ -1,5 +1,7 @@
1
1
  import { get } from '@ember/object';
2
2
 
3
+ export { buildIdentityStub } from '@fleetbase/fleetops-data/utils/identity-stub';
4
+
3
5
  export function resolveIdentityCellResource(args) {
4
6
  const column = args.column ?? {};
5
7
  const resourcePath = column.resourcePath;
@@ -18,59 +20,3 @@ export function resolveIdentityCellResource(args) {
18
20
 
19
21
  return args.row;
20
22
  }
21
-
22
- const inflight = new WeakMap();
23
-
24
- /**
25
- * A stand-in for a related record a row only knows by name: enough for an
26
- * identity cell to render (`name`, `resourceType`) and a `loadResource()`
27
- * the cell, pill or summary calls when the real record is needed. The
28
- * stub never carries a UUID in an identifier field, so nothing renders one
29
- * as a plate or an id, and concurrent loads of the same row share one
30
- * request.
31
- *
32
- * `load` receives the row and returns the record (or a promise of it); with
33
- * no `load`, `loadResource()` resolves to null and the stub stays static.
34
- */
35
- export function buildIdentityStub(row, { type, nameKey = `${type}_name`, name, load, extra = {} } = {}) {
36
- const label = name ?? (row ? get(row, nameKey) : null);
37
-
38
- if (!label) {
39
- return null;
40
- }
41
-
42
- const stub = {
43
- ...extra,
44
- name: label,
45
- display_name: label,
46
- displayName: label,
47
- resourceType: type,
48
- isIdentityStub: true,
49
- loadResource: async () => {
50
- if (typeof load !== 'function') {
51
- return null;
52
- }
53
-
54
- let loads = inflight.get(row);
55
-
56
- if (!loads) {
57
- loads = new Map();
58
- inflight.set(row, loads);
59
- }
60
-
61
- if (!loads.has(type)) {
62
- loads.set(
63
- type,
64
- Promise.resolve()
65
- .then(() => load(row))
66
- .catch(() => null)
67
- .finally(() => loads.delete(type))
68
- );
69
- }
70
-
71
- return loads.get(type);
72
- },
73
- };
74
-
75
- return stub;
76
- }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Helpers for the company's default live map view.
3
+ *
4
+ * Where the live map opens, in order of precedence:
5
+ * 1. "Fit to my vehicles" is on and at least one vehicle has a position:
6
+ * the map fits every positioned vehicle once it has loaded.
7
+ * 2. The company has a default centre: the map opens there at the
8
+ * configured zoom. An admin's explicit choice wins over browser
9
+ * geolocation and active order destinations.
10
+ * 3. Otherwise the user's location: active order destinations, then
11
+ * browser geolocation, then the IP lookup.
12
+ * 4. The centre of the company's country (or the country of its timezone).
13
+ * 5. The hardcoded fallback.
14
+ */
15
+
16
+ /** Zoom used for a company-defined centre when no zoom was configured. */
17
+ export const DEFAULT_CENTER_ZOOM = 12;
18
+
19
+ /** Zoom used when the map falls back to the centre of the company's country. */
20
+ export const COUNTRY_CENTER_ZOOM = 4;
21
+
22
+ /** Closest the map zooms when fitting vehicles, so a single vehicle is not shown at street level. */
23
+ export const FIT_VEHICLES_MAX_ZOOM = 15;
24
+
25
+ export function isLatitude(value) {
26
+ return typeof value === 'number' && Number.isFinite(value) && value >= -90 && value <= 90;
27
+ }
28
+
29
+ export function isLongitude(value) {
30
+ return typeof value === 'number' && Number.isFinite(value) && value >= -180 && value <= 180;
31
+ }
32
+
33
+ export function isZoom(value) {
34
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 20;
35
+ }
36
+
37
+ /**
38
+ * Parse a user-entered coordinate, returning null for blank or invalid input.
39
+ *
40
+ * @param {*} value
41
+ * @return {Number|null}
42
+ */
43
+ export function toCoordinate(value) {
44
+ if (value === null || value === undefined || (typeof value === 'string' && value.trim() === '')) {
45
+ return null;
46
+ }
47
+
48
+ const number = Number(value);
49
+ return Number.isFinite(number) ? number : null;
50
+ }
51
+
52
+ /**
53
+ * The company's configured default centre, or null when none is set.
54
+ *
55
+ * @param {Object} settings map settings from `fleet-ops/settings/map`
56
+ * @return {{latitude: Number, longitude: Number, zoom: Number}|null}
57
+ */
58
+ export function defaultMapCenter(settings = {}) {
59
+ const latitude = toCoordinate(settings?.defaultMapLatitude);
60
+ const longitude = toCoordinate(settings?.defaultMapLongitude);
61
+ if (!isLatitude(latitude) || !isLongitude(longitude)) {
62
+ return null;
63
+ }
64
+
65
+ const zoom = toCoordinate(settings?.defaultMapZoom);
66
+ return { latitude, longitude, zoom: isZoom(zoom) ? zoom : DEFAULT_CENTER_ZOOM };
67
+ }
68
+
69
+ /**
70
+ * The centre of the company's country, derived by the server, or null.
71
+ *
72
+ * @param {Object} settings map settings from `fleet-ops/settings/map`
73
+ * @return {{latitude: Number, longitude: Number, zoom: Number}|null}
74
+ */
75
+ export function companyCountryCenter(settings = {}) {
76
+ const { latitude, longitude } = settings?.companyMapCenter ?? {};
77
+ if (!isLatitude(latitude) || !isLongitude(longitude)) {
78
+ return null;
79
+ }
80
+
81
+ return { latitude, longitude, zoom: COUNTRY_CENTER_ZOOM };
82
+ }
83
+
84
+ /**
85
+ * Extract `{ latitude, longitude }` from a GeoJSON point, a `[lat, lng]` pair
86
+ * or a `{ latitude, longitude }` object. Null Island (0,0) is treated as "no
87
+ * position" because that is what unpositioned records default to.
88
+ *
89
+ * @param {*} location
90
+ * @return {{latitude: Number, longitude: Number}|null}
91
+ */
92
+ export function coordinatesOf(location) {
93
+ let latitude;
94
+ let longitude;
95
+
96
+ if (Array.isArray(location?.coordinates)) {
97
+ [longitude, latitude] = location.coordinates;
98
+ } else if (Array.isArray(location)) {
99
+ [latitude, longitude] = location;
100
+ } else {
101
+ latitude = location?.latitude;
102
+ longitude = location?.longitude;
103
+ }
104
+
105
+ if (!isLatitude(latitude) || !isLongitude(longitude) || (latitude === 0 && longitude === 0)) {
106
+ return null;
107
+ }
108
+
109
+ return { latitude, longitude };
110
+ }
111
+
112
+ /**
113
+ * Bounds `[[south, west], [north, east]]` around every record that has a
114
+ * position, or null when none do.
115
+ *
116
+ * @param {Array} records records with a `location` (e.g. live vehicles)
117
+ * @return {Array|null}
118
+ */
119
+ export function positionedBounds(records = []) {
120
+ const points = (Array.isArray(records) ? records : []).map((record) => coordinatesOf(record?.location)).filter(Boolean);
121
+ if (points.length === 0) {
122
+ return null;
123
+ }
124
+
125
+ const latitudes = points.map((point) => point.latitude);
126
+ const longitudes = points.map((point) => point.longitude);
127
+
128
+ return [
129
+ [Math.min(...latitudes), Math.min(...longitudes)],
130
+ [Math.max(...latitudes), Math.max(...longitudes)],
131
+ ];
132
+ }