@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
@@ -9,6 +9,7 @@ import { renderCompleted, waitForInsertedAndSized } from '@fleetbase/ember-ui/ut
9
9
  import LeafletTrackingMarkerComponent from '../leaflet-tracking-marker';
10
10
  import ensureLeafletDrawEditNamespace from '../../utils/leaflet-draw-namespace-guard';
11
11
  import { DEFAULT_LEAFLET_TILE_ATTRIBUTION } from '../../utils/leaflet-tile-url';
12
+ import { FIT_VEHICLES_MAX_ZOOM, positionedBounds } from '../../utils/map-default-view';
12
13
 
13
14
  const DEFAULT_CENTER = [1.31, 103.85];
14
15
  const DEFAULT_ZOOM = 11;
@@ -56,6 +57,8 @@ export default class WidgetLiveFleetComponent extends Component {
56
57
  #map = null;
57
58
  #mapSetPromise = null;
58
59
  #resolveMapSet = null;
60
+ /** Fit to the vehicles only on the first load, never on the periodic reconcile. */
61
+ #fittedToVehicles = false;
59
62
 
60
63
  constructor() {
61
64
  super(...arguments);
@@ -152,15 +155,57 @@ export default class WidgetLiveFleetComponent extends Component {
152
155
  return this.drivers.length + this.vehicles.length;
153
156
  }
154
157
 
155
- /** Center on the first available marker (driver, then vehicle), else default. */
156
- get center() {
158
+ /**
159
+ * Bounds around the positioned vehicles when the company chose "Fit to my
160
+ * vehicles" (Settings > Map), else null.
161
+ */
162
+ get vehicleBounds() {
163
+ if (!this.mapSettings.fitMapToVehicles) return null;
164
+ return positionedBounds(this.vehicles.map((vehicle) => ({ location: [vehicle.lat, vehicle.lng] })));
165
+ }
166
+
167
+ /**
168
+ * Where the map opens, in order: the positioned vehicles (when fitting is
169
+ * on), the company's default centre, the first positioned marker, the
170
+ * centre of the company's country, the hardcoded default.
171
+ */
172
+ get initialView() {
173
+ const bounds = this.vehicleBounds;
174
+ if (bounds) {
175
+ const [[south, west], [north, east]] = bounds;
176
+ return { center: [(south + north) / 2, (west + east) / 2], zoom: DEFAULT_ZOOM, bounds };
177
+ }
178
+
179
+ const defaultCenter = this.mapSettings.defaultMapCenter;
180
+ if (defaultCenter) return { center: [defaultCenter.latitude, defaultCenter.longitude], zoom: defaultCenter.zoom };
181
+
157
182
  const first = this.drivers[0] ?? this.vehicles[0];
158
- if (first?.lat && first?.lng) return [first.lat, first.lng];
159
- return DEFAULT_CENTER;
183
+ if (first?.lat && first?.lng) return { center: [first.lat, first.lng], zoom: DEFAULT_ZOOM };
184
+
185
+ const countryCenter = this.mapSettings.companyCountryCenter;
186
+ if (countryCenter) return { center: [countryCenter.latitude, countryCenter.longitude], zoom: countryCenter.zoom };
187
+
188
+ return { center: DEFAULT_CENTER, zoom: DEFAULT_ZOOM };
189
+ }
190
+
191
+ get center() {
192
+ return this.initialView.center;
160
193
  }
161
194
 
162
195
  get zoom() {
163
- return DEFAULT_ZOOM;
196
+ return this.initialView.zoom;
197
+ }
198
+
199
+ /** Frame the positioned vehicles once, after the first data load. */
200
+ fitToVehicles(map) {
201
+ if (this.#fittedToVehicles) return false;
202
+
203
+ const bounds = this.vehicleBounds;
204
+ if (!bounds || typeof map?.fitBounds !== 'function') return false;
205
+
206
+ map.fitBounds(bounds, { padding: [30, 30], maxZoom: FIT_VEHICLES_MAX_ZOOM });
207
+ this.#fittedToVehicles = true;
208
+ return true;
164
209
  }
165
210
 
166
211
  statusColor(status) {
@@ -182,9 +227,12 @@ export default class WidgetLiveFleetComponent extends Component {
182
227
  // container before we add markers. Without this, markers added to a
183
228
  // not-yet-sized map silently no-op (the data arrives, count chip ticks
184
229
  // up, but nothing paints).
185
- yield this.ensureInteractive();
230
+ const map = yield this.ensureInteractive();
186
231
  this.data = yield this.fetch.get('fleet-ops/analytics/live-fleet');
187
232
  this.error = null;
233
+ // The fit choice lives in the company's map settings, loaded in the constructor.
234
+ yield this.mapSettings.load();
235
+ this.fitToVehicles(map);
188
236
  } catch (e) {
189
237
  this.error = e?.message ?? 'Failed to load live fleet';
190
238
  }
@@ -0,0 +1,49 @@
1
+ <div class="fleet-ops-widget fleet-ops-proof-of-delivery flex flex-col h-full {{@class}}" data-test-proof-of-delivery-widget ...attributes>
2
+ <div class="flex items-center justify-between px-3 py-1.5 border dark:border-gray-700 border-gray-200 dark:bg-gray-800 bg-gray-50 rounded-t-md shadow-sm">
3
+ <span class="text-xs font-bold text-black dark:text-gray-100">{{t "widget.proof-of-delivery.title"}}</span>
4
+ <FaIcon @icon="camera" class="text-gray-400" />
5
+ </div>
6
+
7
+ <div class="flex-1 p-2 border border-t-0 dark:border-gray-700 border-gray-200 rounded-b-md bg-white dark:bg-gray-900 overflow-auto min-h-0">
8
+ {{#if this.load.isRunning}}
9
+ <div class="flex items-center justify-center h-full"><Spinner /></div>
10
+ {{else if this.error}}
11
+ <div class="flex items-center justify-center h-full text-xs text-red-500">{{this.error}}</div>
12
+ {{else if this.photos.length}}
13
+ <div class="grid grid-cols-3 gap-2">
14
+ {{#each this.photos as |proof|}}
15
+ <div class="min-w-0" data-test-proof-of-delivery-photo={{proof.id}}>
16
+ <button
17
+ type="button"
18
+ class="block w-full aspect-square rounded-md overflow-hidden border dark:border-gray-700 border-gray-200 bg-gray-100 dark:bg-gray-800 hover:opacity-90"
19
+ title={{or proof.remarks (t "widget.proof-of-delivery.open-photo")}}
20
+ aria-label={{t "widget.proof-of-delivery.open-photo"}}
21
+ {{on "click" (fn this.openPhoto proof)}}
22
+ >
23
+ <img src={{proof.url}} alt={{or proof.remarks (t "widget.proof-of-delivery.title")}} loading="lazy" class="w-full h-full object-cover" />
24
+ </button>
25
+ <div class="mt-0.5 leading-tight">
26
+ {{#if proof.order_id}}
27
+ <LinkTo
28
+ @route={{this.orderRoute}}
29
+ @model={{proof.order_id}}
30
+ class="block text-[10px] font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-300 dark:hover:text-blue-200 truncate"
31
+ title={{proof.customer_name}}
32
+ data-test-proof-of-delivery-order
33
+ >{{proof.order_id}}</LinkTo>
34
+ {{/if}}
35
+ {{#if proof.timeAgo}}
36
+ <div class="text-[9px] text-gray-500 dark:text-gray-400 truncate">{{proof.timeAgo}}</div>
37
+ {{/if}}
38
+ </div>
39
+ </div>
40
+ {{/each}}
41
+ </div>
42
+ {{else}}
43
+ <div class="flex flex-col items-center justify-center h-full text-center text-gray-500 dark:text-gray-400" data-test-proof-of-delivery-empty>
44
+ <FaIcon @icon="camera" class="text-lg mb-1 text-gray-300 dark:text-gray-600" />
45
+ <div class="text-[11px]">{{t "widget.proof-of-delivery.empty"}}</div>
46
+ </div>
47
+ {{/if}}
48
+ </div>
49
+ </div>
@@ -0,0 +1,62 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { inject as service } from '@ember/service';
5
+ import { getOwner } from '@ember/application';
6
+ import { task } from 'ember-concurrency';
7
+ import { formatDistanceToNowStrict, isValid } from 'date-fns';
8
+ import config from '../../config/environment';
9
+
10
+ /**
11
+ * The latest proof of delivery photos across the fleet, each linking to its order.
12
+ */
13
+ export default class WidgetProofOfDeliveryComponent extends Component {
14
+ static widgetId = 'fleet-ops-proof-of-delivery-widget';
15
+ static limit = 12;
16
+
17
+ @service fetch;
18
+
19
+ @tracked proofs = [];
20
+ @tracked error = null;
21
+
22
+ constructor() {
23
+ super(...arguments);
24
+ this.load.perform();
25
+ }
26
+
27
+ get photos() {
28
+ return (this.proofs ?? [])
29
+ .filter((proof) => proof?.url)
30
+ .map((proof) => ({
31
+ ...proof,
32
+ timeAgo: this.timeAgo(proof.captured_at),
33
+ }));
34
+ }
35
+
36
+ get orderRoute() {
37
+ // Dashboard widgets render under the host owner, outside the engine's routing context.
38
+ return getOwner(this).mountPoint ? 'operations.orders.index.details' : `${config.mountedEngineRoutePrefix}.operations.orders.index.details`;
39
+ }
40
+
41
+ timeAgo(capturedAt) {
42
+ const date = capturedAt ? new Date(capturedAt) : null;
43
+
44
+ return date && isValid(date) ? formatDistanceToNowStrict(date, { addSuffix: true }) : null;
45
+ }
46
+
47
+ @action openPhoto(proof) {
48
+ if (proof?.url) {
49
+ window.open(proof.url, '_blank', 'noopener,noreferrer');
50
+ }
51
+ }
52
+
53
+ @task({ restartable: true }) *load() {
54
+ this.error = null;
55
+ try {
56
+ const response = yield this.fetch.get('fleet-ops/proofs/recent', { limit: WidgetProofOfDeliveryComponent.limit });
57
+ this.proofs = response?.proofs ?? [];
58
+ } catch (err) {
59
+ this.error = err?.message ?? 'Failed to load';
60
+ }
61
+ }
62
+ }
@@ -175,7 +175,7 @@
175
175
  </div>
176
176
  <InputGroup @name="Opened At">
177
177
  <DatePicker
178
- @value={{or (format-date-fns @resource.opened_at "dd-MM-yyyy") null}}
178
+ @value={{or (format-date-fns @resource.opened_at "yyyy-MM-dd") null}}
179
179
  @onChange={{fn (mut @resource.opened_at)}}
180
180
  @placeholder="DD-MM-YYYY"
181
181
  class="filter-date-input w-full flex-1"
@@ -183,7 +183,7 @@
183
183
  </InputGroup>
184
184
  <InputGroup @name="Due At">
185
185
  <DatePicker
186
- @value={{or (format-date-fns @resource.due_at "dd-MM-yyyy") null}}
186
+ @value={{or (format-date-fns @resource.due_at "yyyy-MM-dd") null}}
187
187
  @onChange={{fn (mut @resource.due_at)}}
188
188
  @placeholder="DD-MM-YYYY"
189
189
  class="filter-date-input w-full flex-1"
@@ -191,7 +191,7 @@
191
191
  </InputGroup>
192
192
  <InputGroup @name="Closed At">
193
193
  <DatePicker
194
- @value={{or (format-date-fns @resource.closed_at "dd-MM-yyyy") null}}
194
+ @value={{or (format-date-fns @resource.closed_at "yyyy-MM-dd") null}}
195
195
  @onChange={{fn (mut @resource.closed_at)}}
196
196
  @placeholder="DD-MM-YYYY"
197
197
  class="filter-date-input w-full flex-1"
@@ -1,4 +1,5 @@
1
1
  import Controller, { inject as controller } from '@ember/controller';
2
+ import { getOwner } from '@ember/application';
2
3
  import { tracked } from '@glimmer/tracking';
3
4
  import { action } from '@ember/object';
4
5
  import { inject as service } from '@ember/service';
@@ -78,6 +79,9 @@ export default class OperationsOrdersIndexDetailsController extends Controller {
78
79
  return [
79
80
  {
80
81
  route: 'operations.orders.index.details.index',
82
+ // Pass the order explicitly: without it the link's href is generated from whatever
83
+ // transition is in flight, and leaving for a shallower route throws in the router.
84
+ model: this.model?.public_id,
81
85
  label: 'Overview',
82
86
  icon: 'folder-open',
83
87
  },
@@ -219,10 +223,22 @@ export default class OperationsOrdersIndexDetailsController extends Controller {
219
223
  }
220
224
 
221
225
  @task *refresh() {
226
+ yield this.refreshDetails();
227
+ }
228
+
229
+ /**
230
+ * Reload only the order details route. A bare `hostRouter.refresh()` re-runs every route
231
+ * from the application down (console, the engine's application `beforeModel`, the orders
232
+ * list), so a socket event during navigation leaves a slow refresh in flight.
233
+ */
234
+ async refreshDetails() {
222
235
  try {
223
- yield this.hostRouter.refresh();
236
+ await getOwner(this).lookup('route:operations.orders.index.details').refresh();
224
237
  } catch (error) {
225
- return error;
238
+ // A refresh superseded by a newer transition is expected, not an error.
239
+ if (error?.name !== 'TransitionAborted') {
240
+ throw error;
241
+ }
226
242
  }
227
243
  }
228
244
 
@@ -246,7 +262,7 @@ export default class OperationsOrdersIndexDetailsController extends Controller {
246
262
  this.model,
247
263
  async (_msg, { reloadable }) => {
248
264
  if (reloadable) {
249
- await this.hostRouter.refresh();
265
+ await this.refreshDetails();
250
266
  }
251
267
  },
252
268
  { debounceMs: 250 }
@@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
4
4
  import { action, computed } from '@ember/object';
5
5
  import { isNone } from '@ember/utils';
6
6
  import { isValid as isValidDate } from 'date-fns';
7
+ import { later } from '@ember/runloop';
7
8
  import { task } from 'ember-concurrency';
8
9
  import isObject from '@fleetbase/ember-core/utils/is-object';
9
10
  import isJson from '@fleetbase/ember-core/utils/is-json';
@@ -11,6 +12,9 @@ import createFullCalendarEventFromOrder from '../../../utils/create-full-calenda
11
12
  import createFullCalendarEventFromScheduleItem from '../../../utils/create-full-calendar-event-from-schedule-item';
12
13
  import toCalendarDate from '../../../utils/to-calendar-date';
13
14
 
15
+ // The ember-core socket service subscribes ~300 ms after listen() is called.
16
+ const SOCKET_SUBSCRIBE_SETTLE_MS = 500;
17
+
14
18
  /**
15
19
  * OperationsSchedulerIndexController
16
20
  *
@@ -722,28 +726,55 @@ export default class OperationsSchedulerIndexController extends Controller {
722
726
  // Real-Time Socket Subscriptions
723
727
  // -------------------------------------------------------------------------
724
728
 
729
+ // Names of the socket channels this board opened, so teardown closes only those
730
+ // and leaves every other subscription (chat, notifications, ...) alone.
731
+ _socketChannelNames = new Set();
732
+
725
733
  @action async subscribeToRealTimeUpdates() {
726
- const orgId = this.currentUser?.companyId ?? this.currentUser?.company?.id;
727
- if (!orgId) return;
728
- await this.socket.listen(`company.${orgId}.orders`, (payload) => this._handleOrderSocketEvent(payload));
729
- this.drivers.forEach(async (driver) => {
730
- await this.socket.listen(`driver.${driver.id}`, (payload) => this._handleDriverSocketEvent(payload));
731
- });
734
+ const drivers = this.drivers ?? [];
735
+ await Promise.all(
736
+ drivers.map((driver) => {
737
+ if (!driver?.id) return;
738
+ const channelName = `driver.${driver.id}`;
739
+ if (this._socketChannelNames.has(channelName)) return;
740
+ this._socketChannelNames.add(channelName);
741
+ return this.socket.listen(channelName, (payload) => this._handleDriverSocketEvent(payload));
742
+ })
743
+ );
732
744
  }
733
745
 
734
746
  @action unsubscribeFromRealTimeUpdates() {
735
- if (this.socket && typeof this.socket.closeChannels === 'function') {
736
- this.socket.closeChannels();
747
+ const names = this._socketChannelNames;
748
+ this._socketChannelNames = new Set();
749
+ if (!this.socket || names.size === 0) return;
750
+
751
+ if (typeof this.socket.closeChannel === 'function') {
752
+ names.forEach((name) => this.socket.closeChannel(name));
753
+ return;
737
754
  }
755
+
756
+ const remaining = this._closeOpenedChannels(names);
757
+ if (remaining.size === 0) return;
758
+
759
+ // The socket service subscribes after a short delay, so a channel requested just before
760
+ // teardown may not exist yet: sweep once more after that delay.
761
+ later(this, () => this._closeOpenedChannels(remaining), SOCKET_SUBSCRIBE_SETTLE_MS);
738
762
  }
739
763
 
740
- _handleOrderSocketEvent({ data } = {}) {
741
- if (!data?.id) return;
742
- try {
743
- this.store.pushPayload('order', { order: data });
744
- } catch {
745
- /* ignore */
764
+ /**
765
+ * Close the socket's channels with the given names, except any the board has opened again
766
+ * since. Returns the names that had no channel yet.
767
+ */
768
+ _closeOpenedChannels(names) {
769
+ const remaining = new Set([...names].filter((name) => !this._socketChannelNames.has(name)));
770
+ const channels = Array.isArray(this.socket.channels) ? this.socket.channels : [];
771
+ for (const channel of channels) {
772
+ if (!names.has(channel?.name) || this._socketChannelNames.has(channel.name)) continue;
773
+ remaining.delete(channel.name);
774
+ channel.close();
746
775
  }
776
+
777
+ return remaining;
747
778
  }
748
779
 
749
780
  _handleDriverSocketEvent({ event, data } = {}) {
@@ -28,6 +28,12 @@ export default class SettingsIndexController extends Controller {
28
28
  description: 'Keep commerce, metadata, and visual conventions aligned.',
29
29
  links: [
30
30
  { label: 'Payments', route: 'settings.payments', icon: 'cash-register', description: 'Payment setup for operational commerce workflows.' },
31
+ {
32
+ label: 'Tracking Page',
33
+ route: 'settings.tracking-page',
34
+ icon: 'magnifying-glass-location',
35
+ description: 'The page your customers use to follow deliveries: address, branding and how they verify.',
36
+ },
31
37
  {
32
38
  label: 'Telematics',
33
39
  route: 'settings.telematics',
@@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
4
4
  import { action } from '@ember/object';
5
5
  import { task } from 'ember-concurrency';
6
6
  import MAP_PROVIDER_OPTIONS from '../../utils/map-provider-options';
7
+ import { isLatitude, isLongitude, isZoom, toCoordinate } from '../../utils/map-default-view';
7
8
 
8
9
  export default class SettingsMapController extends Controller {
9
10
  @service fetch;
@@ -11,6 +12,7 @@ export default class SettingsMapController extends Controller {
11
12
  @service intl;
12
13
  @service mapManager;
13
14
  @service mapSettings;
15
+ @service location;
14
16
 
15
17
  /**
16
18
  * Available map provider options for the provider selector.
@@ -39,6 +41,21 @@ export default class SettingsMapController extends Controller {
39
41
  @tracked showGoogleMapsTrafficLayer = false;
40
42
  @tracked showGoogleMapsTransitLayer = false;
41
43
 
44
+ /**
45
+ * The company's default live map view. Latitude and longitude are both
46
+ * set or both null; zoom is optional.
47
+ */
48
+ @tracked defaultMapLatitude = null;
49
+ @tracked defaultMapLongitude = null;
50
+ @tracked defaultMapZoom = null;
51
+ @tracked fitMapToVehicles = false;
52
+
53
+ /**
54
+ * Bumped to re-create the coordinates input, which only reads its value
55
+ * when it is constructed (e.g. after clearing the default center).
56
+ */
57
+ @tracked defaultCenterInputKey = 0;
58
+
42
59
  /**
43
60
  * Whether settings have been loaded from the server.
44
61
  *
@@ -74,6 +91,36 @@ export default class SettingsMapController extends Controller {
74
91
  return this.mapProvider === 'leaflet';
75
92
  }
76
93
 
94
+ /**
95
+ * The default center as a GeoJSON point for the coordinates input, or null.
96
+ *
97
+ * @memberof SettingsMapController
98
+ * @return {Object|null}
99
+ */
100
+ get defaultMapCenterPoint() {
101
+ // Not null: CoordinatesInput#isPoint reads `.type` on any object, and typeof null is "object".
102
+ if (!this.hasDefaultMapCenter) {
103
+ return undefined;
104
+ }
105
+
106
+ return { type: 'Point', coordinates: [this.defaultMapLongitude, this.defaultMapLatitude] };
107
+ }
108
+
109
+ get hasDefaultMapCenter() {
110
+ return isLatitude(this.defaultMapLatitude) && isLongitude(this.defaultMapLongitude);
111
+ }
112
+
113
+ /**
114
+ * The zoom to save: a whole number from 1 to 20, else null.
115
+ *
116
+ * @memberof SettingsMapController
117
+ * @return {Number|null}
118
+ */
119
+ get normalizedDefaultMapZoom() {
120
+ const zoom = toCoordinate(this.defaultMapZoom);
121
+ return isZoom(zoom) ? zoom : null;
122
+ }
123
+
77
124
  // ─── Actions ───────────────────────────────────────────────────────────────
78
125
 
79
126
  /**
@@ -86,6 +133,24 @@ export default class SettingsMapController extends Controller {
86
133
  this.mapProvider = option?.value ?? option ?? 'leaflet';
87
134
  }
88
135
 
136
+ /**
137
+ * Receives coordinates typed into, or picked on the map of, the default
138
+ * center input.
139
+ *
140
+ * @param {{latitude: *, longitude: *}} coordinates
141
+ * @memberof SettingsMapController
142
+ */
143
+ @action setDefaultMapCenter({ latitude, longitude } = {}) {
144
+ this.defaultMapLatitude = toCoordinate(latitude);
145
+ this.defaultMapLongitude = toCoordinate(longitude);
146
+ }
147
+
148
+ @action clearDefaultMapCenter() {
149
+ this.defaultMapLatitude = null;
150
+ this.defaultMapLongitude = null;
151
+ this.defaultCenterInputKey++;
152
+ }
153
+
89
154
  @task *saveSettings() {
90
155
  const settings = {
91
156
  mapProvider: this.mapProvider,
@@ -94,12 +159,19 @@ export default class SettingsMapController extends Controller {
94
159
  googleMapsMapType: this.googleMapsMapType,
95
160
  showGoogleMapsTrafficLayer: this.showGoogleMapsTrafficLayer,
96
161
  showGoogleMapsTransitLayer: this.showGoogleMapsTransitLayer,
162
+ defaultMapLatitude: this.hasDefaultMapCenter ? this.defaultMapLatitude : null,
163
+ defaultMapLongitude: this.hasDefaultMapCenter ? this.defaultMapLongitude : null,
164
+ defaultMapZoom: this.normalizedDefaultMapZoom,
165
+ fitMapToVehicles: this.fitMapToVehicles,
97
166
  };
98
167
 
99
168
  try {
100
169
  const response = yield this.fetch.post('fleet-ops/settings/map', { settings });
101
170
  this.mapSettings.applySettings(response);
102
171
 
172
+ // Re-resolve where maps open so an open live map moves to the new default.
173
+ this.location.relocate();
174
+
103
175
  if (this.mapManager && typeof this.mapManager.setActiveProvider === 'function') {
104
176
  this.mapManager.setActiveProvider(this.mapProvider);
105
177
  }
@@ -124,6 +196,9 @@ export default class SettingsMapController extends Controller {
124
196
  this.googleMapsMapType = response?.googleMapsMapType ?? 'roadmap';
125
197
  this.showGoogleMapsTrafficLayer = Boolean(response?.showGoogleMapsTrafficLayer);
126
198
  this.showGoogleMapsTransitLayer = Boolean(response?.showGoogleMapsTransitLayer);
199
+ this.setDefaultMapCenter({ latitude: response?.defaultMapLatitude, longitude: response?.defaultMapLongitude });
200
+ this.defaultMapZoom = response?.defaultMapZoom ?? null;
201
+ this.fitMapToVehicles = Boolean(response?.fitMapToVehicles);
127
202
  this.settingsLoaded = true;
128
203
  } catch (error) {
129
204
  this.notifications.serverError(error);
@@ -17,7 +17,8 @@ export default class SettingsOrchestratorController extends Controller {
17
17
  @service intl;
18
18
  @service('orchestration-engine') engineRegistry;
19
19
 
20
- @tracked allocationEngine = 'vroom';
20
+ @tracked allocationEngine = 'greedy';
21
+ @tracked serverEngines = null;
21
22
  @tracked autoAllocateOnCreate = false;
22
23
  @tracked autoReallocateOnComplete = false;
23
24
  @tracked maxTravelTimeSeconds = 3600;
@@ -26,18 +27,19 @@ export default class SettingsOrchestratorController extends Controller {
26
27
 
27
28
  /**
28
29
  * Engine options for the selector dropdown.
29
- * Populated from the allocation-engine registry so new engines appear
30
- * automatically without modifying this controller.
30
+ * Populated from the engines registered on the server (the ones a run can
31
+ * actually use), falling back to the frontend allocation-engine registry.
31
32
  */
32
33
  get engineOptions() {
33
- return this.engineRegistry.availableEngines;
34
+ return this.serverEngines ?? this.engineRegistry.availableEngines;
34
35
  }
35
36
 
36
37
  @task *loadSettings() {
37
38
  this.isLoading = true;
38
39
  try {
39
- const settings = yield this.fetch.get('fleet-ops/settings/orchestrator-settings');
40
- this.allocationEngine = settings.allocation_engine ?? 'vroom';
40
+ const [settings, engines] = yield Promise.all([this.fetch.get('fleet-ops/settings/orchestrator-settings'), this.fetch.get('fleet-ops/orchestrator/engines').catch(() => null)]);
41
+ this.serverEngines = engines?.engines ?? null;
42
+ this.allocationEngine = settings.allocation_engine ?? 'greedy';
41
43
  this.autoAllocateOnCreate = settings.auto_allocate_on_create ?? false;
42
44
  this.autoReallocateOnComplete = settings.auto_reallocate_on_complete ?? false;
43
45
  this.maxTravelTimeSeconds = settings.max_travel_time_seconds ?? 3600;