@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
@@ -6,7 +6,6 @@ use Fleetbase\FleetOps\Models\TrackingNumber;
6
6
  use Fleetbase\FleetOps\Models\TrackingStatus;
7
7
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
8
8
  use Illuminate\Support\Str;
9
- use Milon\Barcode\Facades\DNS2DFacade as DNS2D;
10
9
 
11
10
  class TrackingNumberObserver
12
11
  {
@@ -17,10 +16,9 @@ class TrackingNumberObserver
17
16
  */
18
17
  public function creating(TrackingNumber $trackingNumber)
19
18
  {
20
- // generate a barcode annd qr code
19
+ // generate the tracking number, then the qr code and barcode printed from it
21
20
  $trackingNumber->tracking_number = $this->generateTrackingNumber($trackingNumber);
22
- $trackingNumber->qr_code = $this->generateBarcode($trackingNumber->owner_uuid, 'QRCODE');
23
- $trackingNumber->barcode = $this->generateBarcode($trackingNumber->owner_uuid, 'PDF417');
21
+ $trackingNumber->fill($this->generateCodeImages($trackingNumber->tracking_number, $this->ownerPublicId($trackingNumber)));
24
22
  }
25
23
 
26
24
  /**
@@ -47,9 +45,23 @@ class TrackingNumberObserver
47
45
  return TrackingNumber::generateNumber($trackingNumber->region);
48
46
  }
49
47
 
50
- protected function generateBarcode(string $ownerUuid, string $type): string
48
+ /**
49
+ * @return array{qr_code: string, barcode: string}
50
+ */
51
+ protected function generateCodeImages(string $trackingNumber, ?string $ownerPublicId): array
51
52
  {
52
- return DNS2D::getBarcodePNG($ownerUuid, $type);
53
+ return TrackingNumber::codeImages($trackingNumber, $ownerPublicId);
54
+ }
55
+
56
+ protected function ownerPublicId(TrackingNumber $trackingNumber): ?string
57
+ {
58
+ if (empty($trackingNumber->owner_uuid)) {
59
+ return null;
60
+ }
61
+
62
+ $publicId = data_get($trackingNumber, 'owner.public_id');
63
+
64
+ return is_string($publicId) && $publicId !== '' ? $publicId : null;
53
65
  }
54
66
 
55
67
  protected function createTrackingStatus(array $attributes): TrackingStatus
@@ -20,6 +20,7 @@ class UserObserver
20
20
 
21
21
  protected function deleteDrivers(string $userUuid): void
22
22
  {
23
- Driver::where('user_uuid', $userUuid)->delete();
23
+ // Delete each driver through the model so DriverObserver cleans up after it
24
+ Driver::where('user_uuid', $userUuid)->get()->each->delete();
24
25
  }
25
26
  }
@@ -66,7 +66,7 @@ class VehicleObserver
66
66
  public function deleted(Vehicle $vehicle)
67
67
  {
68
68
  // Unassign the deleted vehicle from matching driver/(s)
69
- $this->deleteDriversAssignedTo($vehicle);
69
+ $this->unassignDriversFrom($vehicle);
70
70
 
71
71
  $this->invalidateLiveCache();
72
72
  }
@@ -81,9 +81,14 @@ class VehicleObserver
81
81
  return Driver::where('uuid', $identifier)->whereNull('deleted_at')->withoutGlobalScopes()->first();
82
82
  }
83
83
 
84
- protected function deleteDriversAssignedTo(Vehicle $vehicle): mixed
84
+ protected function unassignDriversFrom(Vehicle $vehicle): int
85
85
  {
86
- return Driver::where(['vehicle_uuid' => $vehicle->uuid])->delete();
86
+ $unassigned = Driver::where(['vehicle_uuid' => $vehicle->uuid])->update(['vehicle_uuid' => null]);
87
+ if ($unassigned) {
88
+ Driver::invalidateApiCacheManually($vehicle->company_uuid);
89
+ }
90
+
91
+ return $unassigned;
87
92
  }
88
93
 
89
94
  protected function invalidateLiveCache(): void
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Orchestration\Engines;
4
4
 
5
+ use Fleetbase\FleetOps\Orchestration\Support\RouteMetrics;
5
6
  use Illuminate\Support\Collection;
6
7
 
7
8
  /**
@@ -18,6 +19,8 @@ use Illuminate\Support\Collection;
18
19
  * using a nearest-neighbour TSP heuristic.
19
20
  * 3. Returns one assignment entry per order, preserving the existing
20
21
  * vehicle_id / driver_id and updating only the sequence number.
22
+ * 4. Estimates arrival times and route distance/duration along the
23
+ * sequenced stops (see RouteMetrics).
21
24
  *
22
25
  * This ensures that running "Assign Vehicles" followed by "Optimize Routes"
23
26
  * produces one properly-sequenced route per vehicle rather than re-running
@@ -37,6 +40,9 @@ class RouteSequencingEngine
37
40
  {
38
41
  $assignments = [];
39
42
  $unassigned = [];
43
+ $startTime = now()->timestamp;
44
+ $distance = 0;
45
+ $duration = 0;
40
46
 
41
47
  // Group orders by their currently assigned vehicle UUID
42
48
  $byVehicle = [];
@@ -73,6 +79,13 @@ class RouteSequencingEngine
73
79
  // (or waypoints for multi-drop orders). We keep pickup before its own
74
80
  // dropoff as a hard constraint.
75
81
  $sequenced = $this->_sequenceOrdersForVehicle($vehicleOrders, $startLat, $startLng);
82
+ $metrics = RouteMetrics::measure(
83
+ $startLat !== null && $startLng !== null ? [$startLat, $startLng] : null,
84
+ array_map(fn ($stop) => ['order_id' => $stop['order_public_id'], 'lat' => $stop['lat'], 'lng' => $stop['lng']], $sequenced),
85
+ $startTime
86
+ );
87
+ $distance += $metrics['distance'];
88
+ $duration += $metrics['duration'];
76
89
 
77
90
  // Build assignment entries — one per order, with the sequence number
78
91
  // being the position of the order's FIRST stop in the sequenced list.
@@ -92,9 +105,11 @@ class RouteSequencingEngine
92
105
  'driver_id' => $driverPublicId,
93
106
  'sequence' => $orderSequences[$order->public_id] ?? 1,
94
107
  'waypoint_sequence' => null,
95
- 'arrival' => null,
96
- 'duration' => null,
97
- 'distance' => null,
108
+ 'arrival' => $metrics['orders'][$order->public_id]['arrival'] ?? null,
109
+ 'duration' => $metrics['orders'][$order->public_id]['duration'] ?? null,
110
+ 'distance' => $metrics['orders'][$order->public_id]['distance'] ?? null,
111
+ 'route_distance' => $metrics['distance'],
112
+ 'route_duration' => $metrics['duration'],
98
113
  ];
99
114
  }
100
115
  }
@@ -106,6 +121,9 @@ class RouteSequencingEngine
106
121
  'engine' => 'route_sequencing',
107
122
  'assigned' => count($assignments),
108
123
  'unassigned' => count($unassigned),
124
+ 'distance' => $distance,
125
+ 'duration' => $duration,
126
+ 'metrics' => 'estimated',
109
127
  ],
110
128
  ];
111
129
  }
@@ -37,6 +37,11 @@ use Illuminate\Support\Facades\Log;
37
37
  */
38
38
  class VroomOrchestrationEngine implements OrchestrationEngineInterface
39
39
  {
40
+ /**
41
+ * Arrivals below this (2001-09-09) are offsets from the route start, not unix timestamps.
42
+ */
43
+ protected const ABSOLUTE_TIMESTAMP_THRESHOLD = 1000000000;
44
+
40
45
  /**
41
46
  * The company whose VROOM settings apply to the current allocation run.
42
47
  * Resolved from the orders so queued runs work without a company session.
@@ -202,6 +207,14 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
202
207
 
203
208
  $result = $this->mapVroomResponse($this->callVroom($vroomPayload), $jobIdReverse);
204
209
 
210
+ // Capacity-only solves run on a uniform placeholder matrix, so VROOM's
211
+ // timings are meaningless here; clear them so real estimates replace them.
212
+ $result['assignments'] = array_map(
213
+ fn (array $assignment) => array_merge($assignment, array_fill_keys(['arrival', 'duration', 'distance', 'route_distance', 'route_duration'], null)),
214
+ $result['assignments']
215
+ );
216
+ unset($result['summary']['distance'], $result['summary']['duration']);
217
+
205
218
  $result['summary'] = array_merge($result['summary'] ?? [], [
206
219
  'engine' => 'vroom',
207
220
  'allocation_strategy' => 'capacity_only',
@@ -339,6 +352,7 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
339
352
  protected function mapVroomResponse(array $vroomResult, array $jobIdReverse): array
340
353
  {
341
354
  $assignments = [];
355
+ $startTime = now()->timestamp;
342
356
 
343
357
  foreach ($vroomResult['routes'] ?? [] as $route) {
344
358
  $vehicleDesc = json_decode($route['description'] ?? '{}', true);
@@ -355,13 +369,15 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
355
369
  continue;
356
370
  }
357
371
  $assignments[] = [
358
- 'order_id' => $orderId,
359
- 'vehicle_id' => $vehicleId,
360
- 'driver_id' => $driverId,
361
- 'sequence' => ++$sequence,
362
- 'arrival' => $step['arrival'] ?? null,
363
- 'duration' => $step['duration'] ?? null,
364
- 'distance' => $step['distance'] ?? null,
372
+ 'order_id' => $orderId,
373
+ 'vehicle_id' => $vehicleId,
374
+ 'driver_id' => $driverId,
375
+ 'sequence' => ++$sequence,
376
+ 'arrival' => $this->absoluteArrival($step['arrival'] ?? null, $startTime),
377
+ 'duration' => $step['duration'] ?? null,
378
+ 'distance' => $step['distance'] ?? null,
379
+ 'route_distance' => $route['distance'] ?? null,
380
+ 'route_duration' => $route['duration'] ?? null,
365
381
  ];
366
382
  }
367
383
  }
@@ -374,10 +390,24 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
374
390
  return [
375
391
  'assignments' => $assignments,
376
392
  'unassigned' => array_values(array_unique($unassigned)),
377
- 'summary' => $vroomResult['summary'] ?? [],
393
+ 'summary' => array_merge(['engine' => 'vroom'], $vroomResult['summary'] ?? []),
378
394
  ];
379
395
  }
380
396
 
397
+ /**
398
+ * VROOM reports arrivals on the timeline of the time windows it was given.
399
+ * Without absolute windows that timeline starts at 0, so small values are
400
+ * offsets from the start of the run rather than unix timestamps.
401
+ */
402
+ protected function absoluteArrival($arrival, int $startTime): ?int
403
+ {
404
+ if ($arrival === null) {
405
+ return null;
406
+ }
407
+
408
+ return $arrival < self::ABSOLUTE_TIMESTAMP_THRESHOLD ? $startTime + (int) $arrival : (int) $arrival;
409
+ }
410
+
381
411
  protected function mapTaskToVroomJob(array $task, array &$jobIdReverse): ?array
382
412
  {
383
413
  $stop = $task['stops'][0] ?? null;
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\FleetOps\Orchestration;
4
4
 
5
5
  use Fleetbase\FleetOps\Orchestration\Contracts\OrchestrationEngineInterface;
6
+ use Illuminate\Support\Collection;
6
7
 
7
8
  /**
8
9
  * OrchestrationEngineRegistry.
@@ -10,7 +11,7 @@ use Fleetbase\FleetOps\Orchestration\Contracts\OrchestrationEngineInterface;
10
11
  * A simple service-locator for orchestration engines. Engines register
11
12
  * themselves (typically from a service provider) and the
12
13
  * OrchestrationController resolves the active engine at runtime from the
13
- * FleetOps setting key 'fleetops.orchestrator_engine'.
14
+ * organization's Orchestrator settings (see OrchestratorSettings).
14
15
  *
15
16
  * Third-party extensions can register their own engines by resolving this
16
17
  * registry in their service provider:
@@ -21,6 +22,11 @@ use Fleetbase\FleetOps\Orchestration\Contracts\OrchestrationEngineInterface;
21
22
  */
22
23
  class OrchestrationEngineRegistry
23
24
  {
25
+ /**
26
+ * Built-in engine that runs when the selected engine is missing or unavailable.
27
+ */
28
+ public const FALLBACK_ENGINE = 'greedy';
29
+
24
30
  /**
25
31
  * Registered engines keyed by their identifier.
26
32
  *
@@ -80,4 +86,58 @@ class OrchestrationEngineRegistry
80
86
  {
81
87
  return isset($this->engines[$identifier]);
82
88
  }
89
+
90
+ /**
91
+ * Run the selected engine, falling back to the built-in greedy engine when
92
+ * the selected engine is not registered or reports itself unavailable
93
+ * (a RuntimeException, e.g. VROOM unreachable or misconfigured).
94
+ *
95
+ * The result's summary always names the engine that produced it. A
96
+ * fallback result also carries `requested_engine` and `fallback_reason`
97
+ * so callers can tell the user the plan did not come from their engine.
98
+ *
99
+ * @throws \RuntimeException when the selected engine fails and there is no fallback to run
100
+ */
101
+ public function allocateWithFallback(string $identifier, Collection $orders, Collection $vehicles, array $options = []): array
102
+ {
103
+ try {
104
+ $engine = $this->resolve($identifier);
105
+ $result = $engine->allocate($orders, $vehicles, $options);
106
+ $result['summary'] = array_merge(['engine' => $engine->getIdentifier()], $result['summary'] ?? []);
107
+
108
+ return $result;
109
+ } catch (\RuntimeException $e) {
110
+ if ($identifier === static::FALLBACK_ENGINE || !$this->has(static::FALLBACK_ENGINE)) {
111
+ throw $e;
112
+ }
113
+
114
+ // The selected solvers build multi-stop routes, so the fallback
115
+ // must too or orders would be dropped when vehicles are scarce.
116
+ $result = $this->resolve(static::FALLBACK_ENGINE)->allocate($orders, $vehicles, array_merge(['allow_multi_order' => true], $options));
117
+
118
+ return static::markFallback($result, static::FALLBACK_ENGINE, $identifier, $e->getMessage());
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Record on a result that it came from a different engine than requested, and why.
124
+ *
125
+ * @param bool $warn whether the substitution is a failure the user should be warned about,
126
+ * rather than the expected behaviour for the requested engine
127
+ */
128
+ public static function markFallback(array $result, string $engine, string $requestedEngine, string $reason, bool $warn = true): array
129
+ {
130
+ $result['summary'] = array_merge($result['summary'] ?? [], [
131
+ 'engine' => $engine,
132
+ 'requested_engine' => $requestedEngine,
133
+ 'fallback_reason' => $reason,
134
+ ]);
135
+ if (!$warn) {
136
+ return $result;
137
+ }
138
+
139
+ $result['warning'] = sprintf('The "%s" engine was used instead of "%s": %s', $engine, $requestedEngine, $reason);
140
+
141
+ return $result;
142
+ }
83
143
  }
@@ -0,0 +1,61 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Orchestration\Support;
4
+
5
+ use Fleetbase\Models\Setting;
6
+
7
+ /**
8
+ * OrchestratorSettings.
9
+ *
10
+ * Single reader for the organization's Orchestrator settings. The settings
11
+ * page saves them as one company setting (`fleet-ops.allocation-settings`);
12
+ * every runtime consumer (workbench runs, the public API, auto-allocation)
13
+ * reads them through here so they all see what the organization selected.
14
+ */
15
+ class OrchestratorSettings
16
+ {
17
+ public const KEY = 'fleet-ops.allocation-settings';
18
+
19
+ /**
20
+ * System-level engine key read before the company setting existed.
21
+ * Still honoured as the default for organizations that never saved.
22
+ */
23
+ public const LEGACY_ENGINE_KEY = 'fleetops.orchestrator_engine';
24
+
25
+ public const DEFAULT_ENGINE = 'greedy';
26
+
27
+ public static function defaults(): array
28
+ {
29
+ return [
30
+ 'allocation_engine' => static::DEFAULT_ENGINE,
31
+ 'auto_allocate_on_create' => false,
32
+ 'auto_reallocate_on_complete' => false,
33
+ 'max_travel_time_seconds' => 3600,
34
+ 'balance_workload' => false,
35
+ ];
36
+ }
37
+
38
+ /**
39
+ * Resolve the organization's settings merged over the defaults.
40
+ */
41
+ public static function forCompany(?string $companyUuid): array
42
+ {
43
+ $saved = Setting::lookupForCompany($companyUuid, static::KEY, []);
44
+ $saved = array_filter(is_array($saved) ? $saved : [], fn ($value) => $value !== null && $value !== '');
45
+ $defaults = static::defaults();
46
+
47
+ if (!isset($saved['allocation_engine'])) {
48
+ $defaults['allocation_engine'] = Setting::lookup(static::LEGACY_ENGINE_KEY) ?: static::DEFAULT_ENGINE;
49
+ }
50
+
51
+ return array_merge($defaults, $saved);
52
+ }
53
+
54
+ /**
55
+ * Identifier of the orchestration engine the organization selected.
56
+ */
57
+ public static function engineForCompany(?string $companyUuid): string
58
+ {
59
+ return (string) static::forCompany($companyUuid)['allocation_engine'];
60
+ }
61
+ }
@@ -0,0 +1,165 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Orchestration\Support;
4
+
5
+ use Fleetbase\FleetOps\Support\Utils;
6
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
7
+ use Illuminate\Support\Collection;
8
+
9
+ /**
10
+ * RouteMetrics.
11
+ *
12
+ * Estimates per-stop arrival times and route distance/duration for engines
13
+ * that only decide a sequence (greedy, the built-in route sequencer) or for
14
+ * solver routes that came back without some of those figures.
15
+ *
16
+ * Legs are measured with FleetOps' local distance-matrix calculation
17
+ * (Utils::calculateDrivingDistanceAndTime), which needs no routing provider,
18
+ * so estimates are always available and are flagged as such in the summary.
19
+ *
20
+ * Assignment fields follow the VROOM step convention so every engine
21
+ * produces the same shape:
22
+ * - arrival: unix timestamp the vehicle reaches the order's last stop
23
+ * - distance: cumulative route distance (m) at that stop
24
+ * - duration: cumulative travel time (s) at that stop
25
+ * - route_distance: total distance (m) of the vehicle's route
26
+ * - route_duration: total travel time (s) of the vehicle's route
27
+ */
28
+ class RouteMetrics
29
+ {
30
+ /**
31
+ * Measure an ordered stop list.
32
+ *
33
+ * @param array|null $start [lat, lng] the vehicle starts from, or null to start at the first stop
34
+ * @param array $stops ordered list of ['order_id' => string, 'lat' => float, 'lng' => float]
35
+ * @param int $startTime unix timestamp the route starts
36
+ *
37
+ * @return array{orders: array<string, array{arrival: int, distance: int, duration: int}>, distance: int, duration: int}
38
+ */
39
+ public static function measure(?array $start, array $stops, int $startTime): array
40
+ {
41
+ $distance = 0.0;
42
+ $duration = 0.0;
43
+ $previous = $start;
44
+ $orders = [];
45
+
46
+ foreach ($stops as $stop) {
47
+ $current = [(float) $stop['lat'], (float) $stop['lng']];
48
+
49
+ if ($previous !== null) {
50
+ $leg = Utils::calculateDrivingDistanceAndTime(new Point($previous[0], $previous[1]), new Point($current[0], $current[1]));
51
+ $distance += (float) $leg->distance;
52
+ $duration += (float) $leg->time;
53
+ }
54
+
55
+ // The order's figures are those of its last stop in the route.
56
+ $orders[$stop['order_id']] = [
57
+ 'arrival' => $startTime + (int) round($duration),
58
+ 'distance' => (int) round($distance),
59
+ 'duration' => (int) round($duration),
60
+ ];
61
+ $previous = $current;
62
+ }
63
+
64
+ return [
65
+ 'orders' => $orders,
66
+ 'distance' => (int) round($distance),
67
+ 'duration' => (int) round($duration),
68
+ ];
69
+ }
70
+
71
+ /**
72
+ * Fill in arrival, distance and duration figures the engine did not supply.
73
+ *
74
+ * A route without any engine timing is estimated in full; a route with
75
+ * engine timing keeps the engine's figures and only has gaps filled.
76
+ *
77
+ * @param array $result standard orchestration result
78
+ * @param Collection $orders orders that were orchestrated, keyed or not
79
+ * @param Collection $vehicles vehicles that were available to the run
80
+ */
81
+ public static function annotate(array $result, Collection $orders, Collection $vehicles, ?int $startTime = null): array
82
+ {
83
+ $assignments = $result['assignments'] ?? [];
84
+ if (empty($assignments)) {
85
+ return $result;
86
+ }
87
+
88
+ $startTime = $startTime ?? now()->timestamp;
89
+ $ordersById = $orders->keyBy('public_id');
90
+ $vehiclesById = $vehicles->keyBy('public_id');
91
+ $routes = [];
92
+ $estimated = false;
93
+
94
+ foreach ($assignments as $index => $assignment) {
95
+ $routes[$assignment['vehicle_id'] ?? ''][] = $index;
96
+ }
97
+
98
+ foreach ($routes as $vehicleId => $indexes) {
99
+ usort($indexes, fn ($a, $b) => ($assignments[$a]['sequence'] ?? 0) <=> ($assignments[$b]['sequence'] ?? 0));
100
+
101
+ $hasEngineTiming = collect($indexes)->contains(fn ($i) => ($assignments[$i]['arrival'] ?? null) !== null
102
+ || ($assignments[$i]['route_duration'] ?? null) !== null
103
+ || ($assignments[$i]['route_distance'] ?? null) !== null);
104
+
105
+ $stops = [];
106
+ foreach ($indexes as $i) {
107
+ $order = $ordersById->get($assignments[$i]['order_id'] ?? null);
108
+ if (!$order) {
109
+ continue;
110
+ }
111
+ foreach (OrchestrationPayloadBuilder::buildRouteStops($order) as $stop) {
112
+ $stops[] = ['order_id' => $order->public_id, 'lng' => $stop['location'][0], 'lat' => $stop['location'][1]];
113
+ }
114
+ }
115
+
116
+ $metrics = static::measure(static::vehicleStart($vehiclesById->get($vehicleId), $ordersById, $assignments, $indexes), $stops, $startTime);
117
+
118
+ foreach ($indexes as $i) {
119
+ $orderMetrics = $metrics['orders'][$assignments[$i]['order_id'] ?? ''] ?? null;
120
+ $estimate = [
121
+ 'arrival' => $orderMetrics['arrival'] ?? null,
122
+ 'distance' => $orderMetrics['distance'] ?? null,
123
+ 'duration' => $orderMetrics['duration'] ?? null,
124
+ 'route_distance' => $metrics['distance'],
125
+ 'route_duration' => $metrics['duration'],
126
+ ];
127
+
128
+ foreach ($estimate as $key => $value) {
129
+ if (!$hasEngineTiming || ($assignments[$i][$key] ?? null) === null) {
130
+ $assignments[$i][$key] = $value;
131
+ $estimated = true;
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ $result['assignments'] = $assignments;
138
+
139
+ $summary = $result['summary'] ?? [];
140
+ $routeTotals = collect($routes)->map(fn ($indexes) => $assignments[$indexes[0]]);
141
+ $summary['distance'] ??= (int) $routeTotals->sum('route_distance');
142
+ $summary['duration'] ??= (int) $routeTotals->sum('route_duration');
143
+ $summary['metrics'] = $estimated ? 'estimated' : ($summary['metrics'] ?? 'engine');
144
+ $result['summary'] = $summary;
145
+
146
+ return $result;
147
+ }
148
+
149
+ /**
150
+ * Resolve the [lat, lng] a vehicle starts from: the run's vehicle list
151
+ * first, then the vehicle relation of one of its orders.
152
+ */
153
+ protected static function vehicleStart($vehicle, Collection $ordersById, array $assignments, array $indexes): ?array
154
+ {
155
+ if (!$vehicle) {
156
+ $order = $ordersById->get($assignments[$indexes[0]]['order_id'] ?? null);
157
+ $vehicle = $order?->relationLoaded('vehicle') ? $order->vehicle : null;
158
+ }
159
+
160
+ $driver = $vehicle?->relationLoaded('driver') ? $vehicle->driver : null;
161
+ $location = $driver?->location ?? $vehicle?->location;
162
+
163
+ return $location ? [(float) $location->getLat(), (float) $location->getLng()] : null;
164
+ }
165
+ }
@@ -23,6 +23,17 @@ class EventServiceProvider extends ServiceProvider
23
23
  \Fleetbase\FleetOps\Events\OrderFailed::class => [\Fleetbase\Listeners\SendResourceLifecycleWebhook::class, \Fleetbase\FleetOps\Listeners\NotifyOrderEvent::class],
24
24
  \Fleetbase\FleetOps\Events\OrderReady::class => [\Fleetbase\FleetOps\Listeners\HandleOrderReady::class],
25
25
 
26
+ /*
27
+ * Stop Events
28
+ *
29
+ * Public tracking channels follow waypoint and entity activity. Order status, assignment
30
+ * and ETA changes are published from the order observer; positions from the tracking endpoints.
31
+ */
32
+ \Fleetbase\FleetOps\Events\WaypointActivityChanged::class => [\Fleetbase\FleetOps\Listeners\PublishTrackingUpdates::class],
33
+ \Fleetbase\FleetOps\Events\WaypointCompleted::class => [\Fleetbase\FleetOps\Listeners\PublishTrackingUpdates::class],
34
+ \Fleetbase\FleetOps\Events\EntityActivityChanged::class => [\Fleetbase\FleetOps\Listeners\PublishTrackingUpdates::class],
35
+ \Fleetbase\FleetOps\Events\EntityCompleted::class => [\Fleetbase\FleetOps\Listeners\PublishTrackingUpdates::class],
36
+
26
37
  /*
27
38
  * Geofence Events
28
39
  *
@@ -159,6 +159,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
159
159
  });
160
160
  $this->registerNotifications();
161
161
  $this->registerAiCapabilities();
162
+ $this->registerSocketChannels();
162
163
  $this->registerExpansionsFrom(__DIR__ . '/../Expansions');
163
164
 
164
165
  // Register built-in orchestration engines.
@@ -251,6 +252,8 @@ class FleetOpsServiceProvider extends CoreServiceProvider
251
252
  \Fleetbase\FleetOps\Notifications\LateDeparture::class,
252
253
  \Fleetbase\FleetOps\Notifications\RouteDeviation::class,
253
254
  \Fleetbase\FleetOps\Notifications\ProlongedStoppage::class,
255
+ \Fleetbase\FleetOps\Notifications\OrderChanged::class,
256
+ \Fleetbase\FleetOps\Notifications\OrderUnassigned::class,
254
257
  ]);
255
258
 
256
259
  // Register Notifiables
@@ -265,6 +268,16 @@ class FleetOpsServiceProvider extends CoreServiceProvider
265
268
  ]);
266
269
  }
267
270
 
271
+ /**
272
+ * Register who may subscribe to FleetOps realtime channels, and the driver socket principal.
273
+ */
274
+ protected function registerSocketChannels(): void
275
+ {
276
+ $this->callAfterResolving(\Fleetbase\Support\SocketCluster\SocketChannelRegistry::class, function (\Fleetbase\Support\SocketCluster\SocketChannelRegistry $registry) {
277
+ \Fleetbase\FleetOps\Support\SocketChannels::register($registry);
278
+ });
279
+ }
280
+
268
281
  protected function registerAiCapabilities(): void
269
282
  {
270
283
  if (!Utils::classExists(\Fleetbase\Ai\Support\AiCapabilityRegistry::class)) {
@@ -0,0 +1,76 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\Support\ApiModelCache;
6
+ use Illuminate\Database\Eloquent\Model;
7
+ use Illuminate\Support\Facades\Cache;
8
+
9
+ /**
10
+ * Invalidates API caches at most once per window for writes too frequent to
11
+ * invalidate on every call.
12
+ *
13
+ * Location pings write drivers and vehicles every few seconds per device, and
14
+ * order lists embed both, so invalidating on each ping would keep those lists
15
+ * from ever being served from cache. Throttled, a ping costs one Cache::add,
16
+ * and cached lists show a location at most a window old.
17
+ */
18
+ class ApiCacheThrottle
19
+ {
20
+ /**
21
+ * Seconds between invalidations for location writes.
22
+ */
23
+ public const LOCATION_WINDOW = 60;
24
+
25
+ /**
26
+ * Run a frequent write (a telemetry save) with its model events, but invalidate
27
+ * the model's cached lists at most once per window instead of on every write.
28
+ *
29
+ * Order and food truck lists embed vehicles, so invalidating them on every
30
+ * telemetry event would keep them from ever being served from cache for any
31
+ * company with streaming devices. Needs ApiModelCache::withoutInvalidating()
32
+ * (fleetbase/core-api#301); with an older core the write invalidates as before.
33
+ *
34
+ * @template T
35
+ *
36
+ * @param \Closure(): T $write
37
+ *
38
+ * @return T
39
+ */
40
+ public static function telemetryWrite(Model $model, \Closure $write, int $seconds = self::LOCATION_WINDOW): mixed
41
+ {
42
+ // @codeCoverageIgnoreStart
43
+ // fleetbase/core-api ^1.6.69, which this package requires, always has it; the
44
+ // branch only serves an install that pinned an older core by hand.
45
+ if (!method_exists(ApiModelCache::class, 'withoutInvalidating')) {
46
+ return $write();
47
+ }
48
+ // @codeCoverageIgnoreEnd
49
+
50
+ $result = ApiModelCache::withoutInvalidating($write);
51
+ static::invalidate($model, $model->company_uuid, $seconds);
52
+
53
+ return $result;
54
+ }
55
+
56
+ /**
57
+ * Invalidate the model's table for the company unless that was done within the window.
58
+ */
59
+ public static function invalidate(Model $model, ?string $companyUuid, int $seconds = self::LOCATION_WINDOW): void
60
+ {
61
+ if (!ApiModelCache::isCachingEnabled()) {
62
+ return;
63
+ }
64
+
65
+ $key = 'fleetops:api_cache_throttle:' . $model->getTable() . ':' . ($companyUuid ?? 'all');
66
+
67
+ try {
68
+ if (Cache::add($key, true, $seconds)) {
69
+ ApiModelCache::invalidateModelCache($model, $companyUuid);
70
+ }
71
+ } catch (\Throwable $e) {
72
+ // A location ping must never fail on cache bookkeeping; cached lists then
73
+ // show the location until their TTL. Core versions before #301 can throw here.
74
+ }
75
+ }
76
+ }