@fleetbase/fleetops-engine 0.6.38 → 0.6.40

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 (432) hide show
  1. package/addon/components/activity/form.hbs +2 -12
  2. package/addon/components/activity/logic-builder.hbs +1 -1
  3. package/addon/components/custom-entity/form.hbs +4 -20
  4. package/addon/components/device/form.hbs +3 -15
  5. package/addon/components/device/panel-header.hbs +13 -2
  6. package/addon/components/driver/details.hbs +33 -2
  7. package/addon/components/driver/form.hbs +42 -0
  8. package/addon/components/driver/panel-header.hbs +8 -1
  9. package/addon/components/driver/schedule.hbs +115 -76
  10. package/addon/components/driver/schedule.js +349 -157
  11. package/addon/components/driver-onboard-settings.hbs +2 -8
  12. package/addon/components/entity-field-editing-settings.hbs +2 -5
  13. package/addon/components/equipment/card.hbs +49 -0
  14. package/addon/components/equipment/card.js +6 -0
  15. package/addon/components/equipment/details.hbs +83 -44
  16. package/addon/components/equipment/form.hbs +111 -41
  17. package/addon/components/equipment/form.js +78 -10
  18. package/addon/components/equipment/panel-header.hbs +36 -0
  19. package/addon/components/equipment/panel-header.js +2 -0
  20. package/addon/components/fleet/driver-listing.hbs +3 -1
  21. package/addon/components/fleet/vehicle-listing.hbs +4 -7
  22. package/addon/components/fleet-panel/vehicle-listing.hbs +1 -6
  23. package/addon/components/fuel-report/form.hbs +1 -5
  24. package/addon/components/layout/fleet-ops-sidebar.hbs +43 -38
  25. package/addon/components/layout/fleet-ops-sidebar.js +103 -24
  26. package/addon/components/maintenance/cost-panel.hbs +176 -0
  27. package/addon/components/maintenance/cost-panel.js +241 -0
  28. package/addon/components/maintenance/details.hbs +123 -60
  29. package/addon/components/maintenance/form.hbs +138 -78
  30. package/addon/components/maintenance/form.js +131 -6
  31. package/addon/components/maintenance/panel-header.hbs +31 -0
  32. package/addon/components/maintenance/panel-header.js +2 -0
  33. package/addon/components/maintenance-schedule/details.hbs +260 -0
  34. package/addon/components/maintenance-schedule/details.js +158 -0
  35. package/addon/components/maintenance-schedule/form.hbs +287 -0
  36. package/addon/components/maintenance-schedule/form.js +199 -0
  37. package/addon/components/map/container.hbs +1 -1
  38. package/addon/components/map/drawer/device-event-listing.hbs +1 -1
  39. package/addon/components/map/drawer/driver-listing.hbs +1 -6
  40. package/addon/components/map/drawer/place-listing.hbs +1 -6
  41. package/addon/components/map/drawer/vehicle-listing.hbs +1 -6
  42. package/addon/components/map/drawer.hbs +8 -1
  43. package/addon/components/map/leaflet-live-map.hbs +2 -1
  44. package/addon/components/map/toolbar/visibility-control-panel.hbs +1 -1
  45. package/addon/components/map/toolbar.hbs +4 -29
  46. package/addon/components/modals/add-driver-shift.hbs +155 -0
  47. package/addon/components/modals/add-driver-shift.js +210 -0
  48. package/addon/components/modals/bulk-assign-orders.hbs +67 -0
  49. package/addon/components/modals/driver-shift.hbs +43 -0
  50. package/addon/components/modals/driver-shift.js +56 -0
  51. package/addon/components/modals/entity-form.hbs +5 -28
  52. package/addon/components/modals/orchestrator-import.hbs +351 -0
  53. package/addon/components/modals/orchestrator-import.js +807 -0
  54. package/addon/components/modals/order-config-new-status.hbs +1 -5
  55. package/addon/components/modals/scheduling-conflict.hbs +47 -0
  56. package/addon/components/modals/scheduling-conflict.js +53 -0
  57. package/addon/components/modals/send-work-order.hbs +91 -0
  58. package/addon/components/modals/send-work-order.js +3 -0
  59. package/addon/components/modals/service-area-form.hbs +1 -6
  60. package/addon/components/modals/set-driver-availability.hbs +50 -0
  61. package/addon/components/modals/set-driver-availability.js +57 -0
  62. package/addon/components/modals/update-order-activity.hbs +13 -9
  63. package/addon/components/modals/user-form.hbs +1 -5
  64. package/addon/components/modals/vehicle-form.hbs +17 -102
  65. package/addon/components/modals/vendor-form.hbs +15 -82
  66. package/addon/components/orchestrator/card-fields-settings.hbs +76 -0
  67. package/addon/components/orchestrator/card-fields-settings.js +134 -0
  68. package/addon/components/orchestrator/order-pool.hbs +264 -0
  69. package/addon/components/orchestrator/order-pool.js +394 -0
  70. package/addon/components/orchestrator/phase-builder.hbs +162 -0
  71. package/addon/components/orchestrator/phase-builder.js +162 -0
  72. package/addon/components/orchestrator/plan-viewer.hbs +278 -0
  73. package/addon/components/orchestrator/plan-viewer.js +342 -0
  74. package/addon/components/orchestrator/resource-panel.hbs +301 -0
  75. package/addon/components/orchestrator/resource-panel.js +106 -0
  76. package/addon/components/orchestrator-workbench.hbs +318 -0
  77. package/addon/components/orchestrator-workbench.js +1088 -0
  78. package/addon/components/order/details/custom-fields.hbs +10 -1
  79. package/addon/components/order/details/detail.hbs +37 -2
  80. package/addon/components/order/details/detail.js +0 -1
  81. package/addon/components/order/details/integrated-vendor-details.hbs +4 -4
  82. package/addon/components/order/details/notes.hbs +1 -7
  83. package/addon/components/order/details/route.hbs +1 -5
  84. package/addon/components/order/form/details.hbs +44 -10
  85. package/addon/components/order/form/details.js +57 -0
  86. package/addon/components/order/form/notes.hbs +1 -7
  87. package/addon/components/order/form/payload.hbs +1 -7
  88. package/addon/components/order/form/route.hbs +3 -15
  89. package/addon/components/order/header.hbs +1 -7
  90. package/addon/components/order/route-editor.hbs +4 -25
  91. package/addon/components/order/schedule-card.hbs +102 -95
  92. package/addon/components/order/schedule-card.js +8 -3
  93. package/addon/components/order-config-manager/details.hbs +2 -10
  94. package/addon/components/order-config-manager/entities.hbs +1 -7
  95. package/addon/components/order-config-manager.hbs +1 -6
  96. package/addon/components/part/card.hbs +49 -0
  97. package/addon/components/part/card.js +6 -0
  98. package/addon/components/part/details.hbs +102 -56
  99. package/addon/components/part/form.hbs +131 -56
  100. package/addon/components/part/form.js +78 -11
  101. package/addon/components/part/panel-header.hbs +36 -0
  102. package/addon/components/part/panel-header.js +2 -0
  103. package/addon/components/place/form.hbs +1 -7
  104. package/addon/components/sensor/details.hbs +1 -1
  105. package/addon/components/sensor/form.hbs +5 -3
  106. package/addon/components/sensor/panel-header.hbs +8 -1
  107. package/addon/components/service-area/form.hbs +1 -6
  108. package/addon/components/service-rate/form.hbs +12 -60
  109. package/addon/components/telematic/form.hbs +6 -2
  110. package/addon/components/vehicle/details/maintenance-history.hbs +42 -0
  111. package/addon/components/vehicle/details/maintenance-history.js +32 -0
  112. package/addon/components/vehicle/details/schedules.hbs +40 -0
  113. package/addon/components/vehicle/details/schedules.js +32 -0
  114. package/addon/components/vehicle/details/work-orders.hbs +42 -0
  115. package/addon/components/vehicle/details/work-orders.js +32 -0
  116. package/addon/components/vehicle/details.hbs +52 -0
  117. package/addon/components/vehicle/form.hbs +51 -0
  118. package/addon/components/vehicle/panel-header.hbs +19 -49
  119. package/addon/components/warranty/details.hbs +3 -2
  120. package/addon/components/warranty/form.hbs +3 -17
  121. package/addon/components/work-order/details.hbs +135 -40
  122. package/addon/components/work-order/form.hbs +178 -45
  123. package/addon/components/work-order/form.js +197 -4
  124. package/addon/components/work-order/panel-header.hbs +31 -0
  125. package/addon/components/work-order/panel-header.js +2 -0
  126. package/addon/controllers/connectivity/devices/index/details.js +1 -1
  127. package/addon/controllers/maintenance/equipment/index/details/index.js +0 -1
  128. package/addon/controllers/maintenance/equipment/index/details.js +36 -1
  129. package/addon/controllers/maintenance/equipment/index/edit.js +56 -1
  130. package/addon/controllers/maintenance/equipment/index/new.js +32 -1
  131. package/addon/controllers/maintenance/equipment/index.js +127 -113
  132. package/addon/controllers/maintenance/maintenances/index/details/index.js +3 -0
  133. package/addon/controllers/maintenance/maintenances/index/details.js +54 -0
  134. package/addon/controllers/maintenance/maintenances/index/edit.js +68 -0
  135. package/addon/controllers/maintenance/maintenances/index/new.js +34 -0
  136. package/addon/controllers/maintenance/maintenances/index.js +191 -0
  137. package/addon/controllers/maintenance/parts/index/details/index.js +0 -1
  138. package/addon/controllers/maintenance/parts/index/details.js +36 -1
  139. package/addon/controllers/maintenance/parts/index/edit.js +56 -1
  140. package/addon/controllers/maintenance/parts/index/new.js +32 -1
  141. package/addon/controllers/maintenance/parts/index.js +135 -113
  142. package/addon/controllers/maintenance/schedules/index/details.js +115 -0
  143. package/addon/controllers/maintenance/schedules/index/edit.js +41 -0
  144. package/addon/controllers/maintenance/schedules/index/new.js +33 -0
  145. package/addon/controllers/maintenance/schedules/index.js +280 -0
  146. package/addon/controllers/maintenance/work-orders/index/details.js +41 -1
  147. package/addon/controllers/maintenance/work-orders/index/edit.js +67 -1
  148. package/addon/controllers/maintenance/work-orders/index/new.js +43 -1
  149. package/addon/controllers/maintenance/work-orders/index.js +105 -113
  150. package/addon/controllers/management/drivers/index/details.js +6 -1
  151. package/addon/controllers/management/vehicles/index/details.js +65 -0
  152. package/addon/controllers/management/vehicles/index.js +18 -0
  153. package/addon/controllers/operations/orchestrator.js +10 -0
  154. package/addon/controllers/operations/orders/index.js +6 -0
  155. package/addon/controllers/operations/scheduler/fleet-schedule.js +341 -0
  156. package/addon/controllers/operations/scheduler/index.js +799 -275
  157. package/addon/controllers/operations/scheduler.js +21 -0
  158. package/addon/controllers/settings/orchestrator.js +70 -0
  159. package/addon/controllers/settings/scheduling.js +155 -0
  160. package/addon/extension.js +19 -0
  161. package/addon/instance-initializers/register-vroom-allocation.js +27 -0
  162. package/addon/models/maintenance-schedule.js +61 -0
  163. package/addon/routes/maintenance/equipment/index/details.js +27 -1
  164. package/addon/routes/maintenance/equipment/index/edit.js +27 -1
  165. package/addon/routes/maintenance/maintenances/index/details/index.js +3 -0
  166. package/addon/routes/maintenance/maintenances/index/details.js +29 -0
  167. package/addon/routes/maintenance/maintenances/index/edit.js +29 -0
  168. package/addon/routes/maintenance/maintenances/index/new.js +3 -0
  169. package/addon/routes/maintenance/maintenances/index.js +23 -0
  170. package/addon/routes/maintenance/maintenances.js +3 -0
  171. package/addon/routes/maintenance/parts/index/details.js +27 -1
  172. package/addon/routes/maintenance/parts/index/edit.js +27 -1
  173. package/addon/routes/maintenance/schedules/index/details/index.js +2 -0
  174. package/addon/routes/maintenance/schedules/index/details/work-orders.js +11 -0
  175. package/addon/routes/maintenance/schedules/index/details.js +25 -0
  176. package/addon/routes/maintenance/schedules/index/edit.js +25 -0
  177. package/addon/routes/maintenance/schedules/index/new.js +2 -0
  178. package/addon/routes/maintenance/schedules/index.js +21 -0
  179. package/addon/routes/maintenance/schedules.js +2 -0
  180. package/addon/routes/maintenance/work-orders/index/details.js +27 -1
  181. package/addon/routes/maintenance/work-orders/index/edit.js +27 -1
  182. package/addon/routes/management/vehicles/index/details/maintenance-history.js +3 -0
  183. package/addon/routes/management/vehicles/index/details/schedules.js +3 -0
  184. package/addon/routes/management/vehicles/index/details/work-orders.js +3 -0
  185. package/addon/routes/operations/orchestrator.js +23 -0
  186. package/addon/routes/operations/scheduler/fleet-schedule.js +28 -0
  187. package/addon/routes/operations/scheduler/index.js +48 -26
  188. package/addon/routes/operations/scheduler.js +14 -1
  189. package/addon/routes/settings/orchestrator.js +27 -0
  190. package/addon/routes/settings/scheduling.js +3 -0
  191. package/addon/routes.js +31 -1
  192. package/addon/services/driver-actions.js +40 -7
  193. package/addon/services/driver-scheduling.js +4 -1
  194. package/addon/services/equipment-actions.js +15 -5
  195. package/addon/services/leaflet-map-manager.js +14 -6
  196. package/addon/services/maintenance-actions.js +17 -14
  197. package/addon/services/maintenance-schedule-actions.js +118 -0
  198. package/addon/services/orchestration-engine-interface.js +49 -0
  199. package/addon/services/orchestration-engine.js +74 -0
  200. package/addon/services/order-actions.js +15 -0
  201. package/addon/services/order-allocation.js +116 -0
  202. package/addon/services/part-actions.js +12 -2
  203. package/addon/services/scheduling.js +316 -0
  204. package/addon/services/vehicle-actions.js +70 -7
  205. package/addon/services/vroom-allocation-engine.js +45 -0
  206. package/addon/services/work-order-actions.js +79 -0
  207. package/addon/styles/fleetops-engine.css +1658 -0
  208. package/addon/templates/analytics/reports/index/edit.hbs +1 -1
  209. package/addon/templates/analytics/reports/index/new.hbs +1 -1
  210. package/addon/templates/application.hbs +6 -1
  211. package/addon/templates/connectivity/devices/index/details/events.hbs +0 -1
  212. package/addon/templates/connectivity/devices.hbs +0 -1
  213. package/addon/templates/connectivity/events/index/details.hbs +0 -1
  214. package/addon/templates/connectivity/events.hbs +0 -1
  215. package/addon/templates/connectivity/sensors.hbs +0 -1
  216. package/addon/templates/connectivity/telematics/index/details/devices.hbs +0 -1
  217. package/addon/templates/connectivity/telematics/index/details/events.hbs +0 -1
  218. package/addon/templates/connectivity/telematics/index/details/sensors.hbs +0 -1
  219. package/addon/templates/connectivity/telematics.hbs +0 -1
  220. package/addon/templates/connectivity/tracking.hbs +0 -1
  221. package/addon/templates/connectivity.hbs +0 -1
  222. package/addon/templates/maintenance/equipment/index/details/index.hbs +1 -2
  223. package/addon/templates/maintenance/equipment/index/details.hbs +15 -2
  224. package/addon/templates/maintenance/equipment/index/edit.hbs +12 -2
  225. package/addon/templates/maintenance/equipment/index/new.hbs +1 -2
  226. package/addon/templates/maintenance/equipment/index.hbs +48 -13
  227. package/addon/templates/maintenance/equipment.hbs +0 -1
  228. package/addon/templates/maintenance/maintenances/index/details/index.hbs +1 -0
  229. package/addon/templates/maintenance/maintenances/index/details.hbs +15 -0
  230. package/addon/templates/maintenance/maintenances/index/edit.hbs +12 -0
  231. package/addon/templates/maintenance/maintenances/index/new.hbs +11 -0
  232. package/addon/templates/maintenance/maintenances/index.hbs +14 -0
  233. package/addon/templates/maintenance/maintenances.hbs +1 -0
  234. package/addon/templates/maintenance/parts/index/details/index.hbs +1 -2
  235. package/addon/templates/maintenance/parts/index/details.hbs +15 -2
  236. package/addon/templates/maintenance/parts/index/edit.hbs +12 -2
  237. package/addon/templates/maintenance/parts/index/new.hbs +1 -2
  238. package/addon/templates/maintenance/parts/index.hbs +48 -13
  239. package/addon/templates/maintenance/parts.hbs +0 -1
  240. package/addon/templates/maintenance/schedules/index/details/index.hbs +1 -0
  241. package/addon/templates/maintenance/schedules/index/details/work-orders.hbs +39 -0
  242. package/addon/templates/maintenance/schedules/index/details.hbs +14 -0
  243. package/addon/templates/maintenance/schedules/index/edit.hbs +12 -0
  244. package/addon/templates/maintenance/schedules/index/new.hbs +11 -0
  245. package/addon/templates/maintenance/schedules/index.hbs +40 -0
  246. package/addon/templates/maintenance/schedules.hbs +1 -0
  247. package/addon/templates/maintenance/work-orders/index/details.hbs +2 -1
  248. package/addon/templates/maintenance/work-orders/index/edit.hbs +2 -4
  249. package/addon/templates/maintenance/work-orders/index/new.hbs +1 -2
  250. package/addon/templates/maintenance/work-orders.hbs +0 -1
  251. package/addon/templates/management/contacts/customers/edit.hbs +1 -2
  252. package/addon/templates/management/contacts/customers/new.hbs +1 -2
  253. package/addon/templates/management/contacts/customers.hbs +1 -1
  254. package/addon/templates/management/contacts/index/edit.hbs +1 -2
  255. package/addon/templates/management/contacts/index/new.hbs +1 -2
  256. package/addon/templates/management/drivers/index/details/orders.hbs +0 -1
  257. package/addon/templates/management/drivers/index/edit.hbs +1 -2
  258. package/addon/templates/management/drivers/index/new.hbs +1 -2
  259. package/addon/templates/management/fleets/index/edit.hbs +1 -2
  260. package/addon/templates/management/fleets/index/new.hbs +1 -2
  261. package/addon/templates/management/fleets/index.hbs +1 -2
  262. package/addon/templates/management/fuel-reports/index/edit.hbs +1 -2
  263. package/addon/templates/management/fuel-reports/index/new.hbs +1 -2
  264. package/addon/templates/management/fuel-reports/index.hbs +1 -2
  265. package/addon/templates/management/issues/index/edit.hbs +1 -2
  266. package/addon/templates/management/issues/index/new.hbs +1 -2
  267. package/addon/templates/management/issues/index.hbs +1 -2
  268. package/addon/templates/management/places/index/details/activity.hbs +0 -1
  269. package/addon/templates/management/places/index/details/comments.hbs +0 -1
  270. package/addon/templates/management/places/index/details/documents.hbs +0 -1
  271. package/addon/templates/management/places/index/details/map.hbs +0 -1
  272. package/addon/templates/management/places/index/details/operations.hbs +0 -1
  273. package/addon/templates/management/places/index/details/performance.hbs +0 -1
  274. package/addon/templates/management/places/index/details/rules.hbs +0 -1
  275. package/addon/templates/management/vehicles/index/details/equipment.hbs +0 -1
  276. package/addon/templates/management/vehicles/index/details/maintenance-history.hbs +2 -0
  277. package/addon/templates/management/vehicles/index/details/schedules.hbs +2 -0
  278. package/addon/templates/management/vehicles/index/details/work-orders.hbs +2 -0
  279. package/addon/templates/management/vehicles/index/details.hbs +1 -1
  280. package/addon/templates/management/vehicles/index/edit.hbs +1 -2
  281. package/addon/templates/management/vehicles/index/new.hbs +1 -2
  282. package/addon/templates/management/vendors/index/edit.hbs +1 -2
  283. package/addon/templates/management/vendors/index/new.hbs +1 -2
  284. package/addon/templates/management/vendors/index.hbs +1 -2
  285. package/addon/templates/management/vendors/integrated.hbs +1 -2
  286. package/addon/templates/operations/orchestrator.hbs +1 -0
  287. package/addon/templates/operations/orders/index.hbs +6 -1
  288. package/addon/templates/operations/scheduler/fleet-schedule.hbs +41 -0
  289. package/addon/templates/operations/scheduler/index.hbs +147 -88
  290. package/addon/templates/operations/scheduler.hbs +7 -1
  291. package/addon/templates/settings/avatars.hbs +1 -1
  292. package/addon/templates/settings/orchestrator.hbs +65 -0
  293. package/addon/templates/settings/payments/index.hbs +1 -5
  294. package/addon/templates/settings/scheduling.hbs +82 -0
  295. package/addon/utils/create-full-calendar-event-from-order.js +52 -14
  296. package/addon/utils/create-full-calendar-event-from-schedule-item.js +50 -0
  297. package/addon/utils/fleet-ops-options.js +254 -0
  298. package/addon/utils/route-colors.js +99 -0
  299. package/addon/utils/to-calendar-date.js +70 -0
  300. package/app/components/driver/schedule.js +1 -0
  301. package/app/components/maintenance/cost-panel.js +1 -0
  302. package/app/components/maintenance/panel-header.js +1 -0
  303. package/app/components/maintenance-schedule/details.js +1 -0
  304. package/app/components/maintenance-schedule/form.js +1 -0
  305. package/app/components/modals/add-driver-shift.js +1 -0
  306. package/app/components/modals/bulk-assign-orders.js +1 -0
  307. package/app/components/modals/driver-shift.js +1 -0
  308. package/app/components/modals/orchestrator-import.js +1 -0
  309. package/app/components/modals/scheduling-conflict.js +1 -0
  310. package/app/components/modals/send-work-order.js +1 -0
  311. package/app/components/modals/set-driver-availability.js +1 -0
  312. package/app/components/orchestrator/card-fields-settings.js +1 -0
  313. package/app/components/orchestrator/order-pool.js +1 -0
  314. package/app/components/orchestrator/phase-builder.js +1 -0
  315. package/app/components/orchestrator/plan-viewer.js +1 -0
  316. package/app/components/orchestrator/resource-panel.js +1 -0
  317. package/app/components/orchestrator-workbench.js +1 -0
  318. package/app/components/vehicle/details/maintenance-history.js +1 -0
  319. package/app/components/vehicle/details/schedules.js +1 -0
  320. package/app/components/vehicle/details/work-orders.js +1 -0
  321. package/app/controllers/operations/orchestrator.js +1 -0
  322. package/app/controllers/settings/orchestrator.js +1 -0
  323. package/app/controllers/settings/scheduling.js +1 -0
  324. package/app/routes/operations/orchestrator.js +1 -0
  325. package/app/routes/settings/orchestrator.js +1 -0
  326. package/app/routes/settings/scheduling.js +1 -0
  327. package/app/services/maintenance-schedule-actions.js +1 -0
  328. package/app/services/orchestration-engine-interface.js +1 -0
  329. package/app/services/orchestration-engine.js +1 -0
  330. package/app/services/order-allocation.js +1 -0
  331. package/app/services/scheduling.js +1 -0
  332. package/app/services/vroom-allocation-engine.js +1 -0
  333. package/app/templates/settings/scheduling.js +1 -0
  334. package/app/utils/create-full-calendar-event-from-schedule-item.js +1 -0
  335. package/app/utils/route-colors.js +1 -0
  336. package/composer.json +4 -2
  337. package/extension.json +1 -1
  338. package/package.json +6 -5
  339. package/server/config/fleetops.php +20 -1
  340. package/server/migrations/2025_08_28_054927_create_parts_table.php +2 -2
  341. package/server/migrations/2025_08_28_054932_add_public_id_to_maintenance_tables.php +45 -0
  342. package/server/migrations/2025_09_01_000001_create_maintenance_schedules_table.php +88 -0
  343. package/server/migrations/2026_04_01_000001_fix_monetary_columns_in_parts_table.php +48 -0
  344. package/server/migrations/2026_04_01_000003_add_photo_uuid_to_equipment_and_parts_tables.php +61 -0
  345. package/server/migrations/2026_04_01_000004_add_public_id_to_equipments_table.php +38 -0
  346. package/server/migrations/2026_04_01_000005_add_missing_columns_to_parts_table.php +67 -0
  347. package/server/migrations/2026_04_04_000001_add_reminder_offsets_to_maintenance_schedules.php +44 -0
  348. package/server/migrations/2026_04_08_000001_add_orchestrator_columns_to_vehicles_table.php +53 -0
  349. package/server/migrations/2026_04_08_000002_add_orchestrator_columns_to_drivers_table.php +41 -0
  350. package/server/migrations/2026_04_08_000003_add_orchestrator_columns_to_orders_table.php +38 -0
  351. package/server/migrations/2026_04_08_000004_add_orchestrator_columns_to_payloads_table.php +38 -0
  352. package/server/migrations/2026_04_08_000005_add_orchestrator_columns_to_waypoints_table.php +38 -0
  353. package/server/migrations/2026_04_09_000001_create_manifests_table.php +48 -0
  354. package/server/migrations/2026_04_09_000002_create_manifest_stops_table.php +48 -0
  355. package/server/migrations/2026_04_09_000003_add_manifest_uuid_to_orders_table.php +28 -0
  356. package/server/migrations/2026_04_13_000001_add_pod_notes_columns_to_waypoints_table.php +39 -0
  357. package/server/migrations/2026_04_14_000001_drop_redundant_capacity_columns_from_payloads_table.php +42 -0
  358. package/server/migrations/2026_04_14_000002_rename_capacity_columns_on_vehicles_table.php +48 -0
  359. package/server/resources/views/mail/maintenance-schedule-reminder.blade.php +59 -0
  360. package/server/resources/views/mail/work-order-dispatched.blade.php +67 -0
  361. package/server/src/Auth/Schemas/FleetOps.php +44 -0
  362. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +150 -0
  363. package/server/src/Console/Commands/SendMaintenanceReminders.php +128 -0
  364. package/server/src/Http/Controllers/Api/v1/OrderController.php +15 -2
  365. package/server/src/Http/Controllers/Api/v1/VehicleController.php +18 -2
  366. package/server/src/Http/Controllers/Internal/v1/DriverController.php +1 -0
  367. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +27 -0
  368. package/server/src/Http/Controllers/Internal/v1/LiveController.php +9 -2
  369. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +165 -0
  370. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +304 -0
  371. package/server/src/Http/Controllers/Internal/v1/ManifestController.php +138 -0
  372. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +975 -0
  373. package/server/src/Http/Controllers/Internal/v1/OrderController.php +42 -0
  374. package/server/src/Http/Controllers/Internal/v1/PartController.php +27 -0
  375. package/server/src/Http/Controllers/Internal/v1/SettingController.php +118 -0
  376. package/server/src/Http/Controllers/Internal/v1/Traits/DriverSchedulingTrait.php +214 -0
  377. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +68 -0
  378. package/server/src/Http/Resources/v1/Driver.php +1 -0
  379. package/server/src/Http/Resources/v1/Maintenance.php +138 -0
  380. package/server/src/Http/Resources/v1/MaintenanceSchedule.php +137 -0
  381. package/server/src/Http/Resources/v1/Orchestrator/Order.php +116 -0
  382. package/server/src/Http/Resources/v1/Order.php +7 -4
  383. package/server/src/Http/Resources/v1/Waypoint.php +7 -0
  384. package/server/src/Http/Resources/v1/WorkOrder.php +136 -0
  385. package/server/src/Imports/EquipmentImport.php +32 -0
  386. package/server/src/Imports/MaintenanceImport.php +32 -0
  387. package/server/src/Imports/MaintenanceScheduleImport.php +32 -0
  388. package/server/src/Imports/PartImport.php +32 -0
  389. package/server/src/Imports/WorkOrderImport.php +32 -0
  390. package/server/src/Jobs/ProcessAllocationJob.php +119 -0
  391. package/server/src/Listeners/HandleDeliveryCompletion.php +47 -0
  392. package/server/src/Listeners/NotifyDriverOnShiftChange.php +63 -0
  393. package/server/src/Mail/MaintenanceScheduleReminder.php +68 -0
  394. package/server/src/Mail/WorkOrderDispatched.php +58 -0
  395. package/server/src/Models/Asset.php +2 -2
  396. package/server/src/Models/Device.php +1 -1
  397. package/server/src/Models/Driver.php +82 -4
  398. package/server/src/Models/Equipment.php +62 -2
  399. package/server/src/Models/Maintenance.php +127 -9
  400. package/server/src/Models/MaintenanceSchedule.php +353 -0
  401. package/server/src/Models/Manifest.php +214 -0
  402. package/server/src/Models/ManifestStop.php +162 -0
  403. package/server/src/Models/Order.php +70 -0
  404. package/server/src/Models/OrderConfig.php +5 -2
  405. package/server/src/Models/Part.php +69 -3
  406. package/server/src/Models/Place.php +1 -1
  407. package/server/src/Models/Sensor.php +1 -1
  408. package/server/src/Models/ServiceQuote.php +1 -1
  409. package/server/src/Models/Vehicle.php +18 -1
  410. package/server/src/Models/Warranty.php +1 -1
  411. package/server/src/Models/Waypoint.php +7 -1
  412. package/server/src/Models/WorkOrder.php +122 -12
  413. package/server/src/Notifications/DriverShiftChanged.php +110 -0
  414. package/server/src/Observers/WorkOrderObserver.php +107 -0
  415. package/server/src/Orchestration/Contracts/OrchestrationEngineInterface.php +63 -0
  416. package/server/src/Orchestration/Engines/DriverAssignmentEngine.php +265 -0
  417. package/server/src/Orchestration/Engines/GreedyOrchestrationEngine.php +155 -0
  418. package/server/src/Orchestration/Engines/RouteSequencingEngine.php +272 -0
  419. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +192 -0
  420. package/server/src/Orchestration/OrchestrationEngineRegistry.php +83 -0
  421. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +380 -0
  422. package/server/src/Providers/EventServiceProvider.php +7 -1
  423. package/server/src/Providers/FleetOpsServiceProvider.php +42 -15
  424. package/server/src/routes.php +65 -4
  425. package/translations/ar-ae.yml +44 -12
  426. package/translations/bg-bg.yaml +51 -10
  427. package/translations/en-us.yaml +444 -1
  428. package/translations/fr-fr.yaml +51 -10
  429. package/translations/mn-mn.yaml +51 -10
  430. package/translations/pt-br.yaml +51 -10
  431. package/translations/ru-ru.yaml +51 -10
  432. package/translations/vi-vn.yaml +48 -12
@@ -0,0 +1,265 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Orchestration\Engines;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Fleetbase\FleetOps\Models\Vehicle;
8
+ use Illuminate\Support\Collection;
9
+
10
+ /**
11
+ * DriverAssignmentEngine.
12
+ *
13
+ * Greedy shift-aware engine for matching available drivers to vehicles that
14
+ * have planned orders (vehicle_assigned_uuid set, driver_assigned_uuid null).
15
+ *
16
+ * Matching priority:
17
+ * 1. Driver is online and on an active shift
18
+ * 2. Driver skills satisfy the required skills of the vehicle's planned orders
19
+ * 3. Driver is geographically closest to the vehicle's current location
20
+ *
21
+ * This engine does not use VROOM — it is a deterministic greedy pass.
22
+ * For V2, a VROOM-based multi-depot driver assignment can replace this.
23
+ */
24
+ class DriverAssignmentEngine
25
+ {
26
+ /**
27
+ * Assign available drivers to vehicles with planned orders.
28
+ *
29
+ * @param Collection $orders Orders with vehicle_assigned_uuid set but no driver
30
+ * @param Collection $vehicles Vehicles to assign drivers to
31
+ * @param array $options Options: require_active_shift (bool, default true)
32
+ *
33
+ * @return array{assignments: array, unassigned: array, summary: array}
34
+ */
35
+ public function assign(Collection $orders, Collection $vehicles, array $options = []): array
36
+ {
37
+ $requireActiveShift = $options['require_active_shift'] ?? false;
38
+ $respectSkills = $options['respect_skills'] ?? true;
39
+
40
+ // Collect all company drivers — online status and vehicle linkage are
41
+ // treated as soft preferences (scored), not hard filters.
42
+ $companyUuid = $orders->first()?->company_uuid;
43
+ $availableDrivers = Driver::where('company_uuid', $companyUuid)
44
+ ->with(['scheduleItems'])
45
+ ->get();
46
+
47
+ // Shift-awareness: if require_active_shift is explicitly set, filter
48
+ // to drivers that have an active shift right now. Drivers with NO
49
+ // schedule items at all are treated as always available — only drivers
50
+ // with a schedule but no active shift are excluded.
51
+ if ($requireActiveShift) {
52
+ $availableDrivers = $availableDrivers->filter(function (Driver $driver) {
53
+ $hasSchedule = $driver->scheduleItems && $driver->scheduleItems->isNotEmpty();
54
+ // No schedule → always available
55
+ if (!$hasSchedule) {
56
+ return true;
57
+ }
58
+
59
+ // Has a schedule → must have an active shift right now
60
+ return $driver->activeShiftFor(now()) !== null;
61
+ });
62
+ }
63
+
64
+ if ($availableDrivers->isEmpty()) {
65
+ return [
66
+ 'assignments' => [],
67
+ 'unassigned' => $vehicles->pluck('public_id')->toArray(),
68
+ 'summary' => ['message' => 'No available drivers found.'],
69
+ ];
70
+ }
71
+
72
+ $assignments = [];
73
+ $assignedDrivers = [];
74
+ $assignedVehicles = [];
75
+ $unassignedOrders = [];
76
+
77
+ // Determine if this is a standalone run (no prior vehicle assignment)
78
+ // or a post-vehicle-assignment run.
79
+ $hasVehicleAssignments = $orders->filter(fn ($o) => !empty($o->vehicle_assigned_uuid))->isNotEmpty();
80
+
81
+ if ($hasVehicleAssignments) {
82
+ // ── Post-vehicle-assignment mode ──────────────────────────────────
83
+ // Orders already have a vehicle. Group by vehicle and find the
84
+ // best driver for each vehicle group.
85
+ $ordersByVehicle = $orders->groupBy('vehicle_assigned_uuid');
86
+
87
+ foreach ($vehicles as $vehicle) {
88
+ $vehicleOrders = $ordersByVehicle->get($vehicle->uuid, collect());
89
+ $requiredSkills = $this->aggregateRequiredSkills($vehicleOrders);
90
+
91
+ $bestDriver = $this->findBestDriver(
92
+ $vehicle,
93
+ $availableDrivers->reject(fn ($d) => in_array($d->uuid, $assignedDrivers)),
94
+ $requiredSkills,
95
+ $respectSkills
96
+ );
97
+
98
+ if (!$bestDriver) {
99
+ foreach ($vehicleOrders as $order) {
100
+ $unassignedOrders[] = $order->public_id;
101
+ }
102
+ continue;
103
+ }
104
+
105
+ $assignedDrivers[] = $bestDriver->uuid;
106
+
107
+ foreach ($vehicleOrders as $order) {
108
+ $assignments[] = [
109
+ 'order_id' => $order->public_id,
110
+ 'vehicle_id' => $vehicle->public_id,
111
+ 'driver_id' => $bestDriver->public_id,
112
+ 'sequence' => null,
113
+ ];
114
+ }
115
+ }
116
+ } else {
117
+ // ── Standalone mode ───────────────────────────────────────────────
118
+ // No prior vehicle assignment. Assign each order to the nearest
119
+ // available vehicle + best matching driver pair.
120
+ $availableVehicles = $vehicles->values();
121
+
122
+ foreach ($orders as $order) {
123
+ // Find the nearest unassigned vehicle
124
+ $pickupLat = $order->payload?->pickup?->location?->getLat();
125
+ $pickupLng = $order->payload?->pickup?->location?->getLng();
126
+
127
+ $bestVehicle = $availableVehicles
128
+ ->reject(fn ($v) => in_array($v->uuid, $assignedVehicles))
129
+ ->sortBy(function ($vehicle) use ($pickupLat, $pickupLng) {
130
+ if (!$pickupLat || !$pickupLng || !$vehicle->location) {
131
+ return PHP_INT_MAX;
132
+ }
133
+
134
+ return $this->haversineDistance(
135
+ $pickupLat, $pickupLng,
136
+ $vehicle->location->getLat(), $vehicle->location->getLng()
137
+ );
138
+ })
139
+ ->first();
140
+
141
+ if (!$bestVehicle) {
142
+ $unassignedOrders[] = $order->public_id;
143
+ continue;
144
+ }
145
+
146
+ $requiredSkills = $order->required_skills ?? [];
147
+ $bestDriver = $this->findBestDriver(
148
+ $bestVehicle,
149
+ $availableDrivers->reject(fn ($d) => in_array($d->uuid, $assignedDrivers)),
150
+ $requiredSkills,
151
+ $respectSkills
152
+ );
153
+
154
+ if (!$bestDriver) {
155
+ $unassignedOrders[] = $order->public_id;
156
+ continue;
157
+ }
158
+
159
+ $assignedVehicles[] = $bestVehicle->uuid;
160
+ $assignedDrivers[] = $bestDriver->uuid;
161
+
162
+ $assignments[] = [
163
+ 'order_id' => $order->public_id,
164
+ 'vehicle_id' => $bestVehicle->public_id,
165
+ 'driver_id' => $bestDriver->public_id,
166
+ 'sequence' => null,
167
+ ];
168
+ }
169
+ }
170
+
171
+ return [
172
+ 'assignments' => $assignments,
173
+ 'unassigned' => $unassignedOrders,
174
+ 'summary' => [
175
+ 'drivers_assigned' => count($assignedDrivers),
176
+ 'vehicles_assigned' => count($assignedVehicles),
177
+ ],
178
+ ];
179
+ }
180
+
181
+ /**
182
+ * Find the best available driver for a given vehicle.
183
+ *
184
+ * Scoring:
185
+ * - Skills match: +100 per matching skill
186
+ * - Active shift: +50
187
+ * - Proximity: inversely proportional to distance (max +50)
188
+ */
189
+ protected function findBestDriver(
190
+ Vehicle $vehicle,
191
+ Collection $availableDrivers,
192
+ array $requiredSkills,
193
+ bool $respectSkills,
194
+ ): ?Driver {
195
+ if ($availableDrivers->isEmpty()) {
196
+ return null;
197
+ }
198
+
199
+ $vehicleLat = $vehicle->location?->getLat();
200
+ $vehicleLng = $vehicle->location?->getLng();
201
+
202
+ $scored = $availableDrivers->map(function (Driver $driver) use (
203
+ $requiredSkills, $respectSkills, $vehicleLat, $vehicleLng
204
+ ) {
205
+ $score = 0;
206
+
207
+ // Skills check
208
+ if ($respectSkills && !empty($requiredSkills)) {
209
+ $driverSkills = $driver->skills ?? [];
210
+ $matchCount = count(array_intersect($requiredSkills, $driverSkills));
211
+ if ($matchCount < count($requiredSkills)) {
212
+ // Driver does not meet all required skills — disqualify
213
+ return null;
214
+ }
215
+ $score += $matchCount * 100;
216
+ }
217
+
218
+ // Online bonus
219
+ if ($driver->online) {
220
+ $score += 30;
221
+ }
222
+ // Active shift bonus — only meaningful if the driver has a schedule
223
+ $hasSchedule = $driver->scheduleItems && $driver->scheduleItems->isNotEmpty();
224
+ if ($hasSchedule && $driver->activeShiftFor(now()) !== null) {
225
+ $score += 50;
226
+ }
227
+
228
+ // Proximity bonus (only if vehicle has a known location)
229
+ if ($vehicleLat && $vehicleLng && $driver->location) {
230
+ $distance = $this->haversineDistance(
231
+ $vehicleLat, $vehicleLng,
232
+ $driver->location->getLat(), $driver->location->getLng()
233
+ );
234
+ // Closer = higher score; cap at 50 points for distance ≤ 1 km
235
+ $proximityScore = max(0, 50 - ($distance / 1000));
236
+ $score += $proximityScore;
237
+ }
238
+
239
+ return ['driver' => $driver, 'score' => $score];
240
+ })->filter()->sortByDesc('score');
241
+
242
+ return $scored->first()['driver'] ?? null;
243
+ }
244
+
245
+ /**
246
+ * Aggregate the unique required skills from a collection of orders.
247
+ */
248
+ protected function aggregateRequiredSkills(Collection $orders): array
249
+ {
250
+ return $orders->flatMap(fn ($o) => $o->required_skills ?? [])->unique()->values()->toArray();
251
+ }
252
+
253
+ /**
254
+ * Calculate the Haversine distance in metres between two lat/lng points.
255
+ */
256
+ protected function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float
257
+ {
258
+ $earthRadius = 6371000; // metres
259
+ $dLat = deg2rad($lat2 - $lat1);
260
+ $dLng = deg2rad($lng2 - $lng1);
261
+ $a = sin($dLat / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2;
262
+
263
+ return $earthRadius * 2 * atan2(sqrt($a), sqrt(1 - $a));
264
+ }
265
+ }
@@ -0,0 +1,155 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Orchestration\Engines;
4
+
5
+ use Fleetbase\FleetOps\Orchestration\Contracts\OrchestrationEngineInterface;
6
+ use Illuminate\Support\Collection;
7
+
8
+ /**
9
+ * GreedyOrchestrationEngine.
10
+ *
11
+ * A simple, dependency-free allocation engine that assigns orders to vehicles
12
+ * without requiring an external routing service such as VROOM.
13
+ *
14
+ * Algorithm:
15
+ * 1. Sort orders by priority (descending) then scheduled_at (ascending).
16
+ * 2. For each order, find the nearest available vehicle (by Haversine distance
17
+ * from the vehicle/driver location to the order pickup point).
18
+ * 3. Assign the order to that vehicle and mark the vehicle as "occupied" for
19
+ * the purpose of this run (one order per vehicle per run by default; pass
20
+ * `allow_multi_order = true` in $options to allow multiple orders per vehicle).
21
+ *
22
+ * This engine is registered as 'greedy' in the OrchestrationEngineRegistry and is
23
+ * used automatically when VROOM is unavailable or when the user explicitly
24
+ * selects it in the orchestrator settings.
25
+ */
26
+ class GreedyOrchestrationEngine implements OrchestrationEngineInterface
27
+ {
28
+ public function getName(): string
29
+ {
30
+ return 'Greedy (built-in)';
31
+ }
32
+
33
+ public function getIdentifier(): string
34
+ {
35
+ return 'greedy';
36
+ }
37
+
38
+ /**
39
+ * Run the greedy nearest-vehicle assignment.
40
+ */
41
+ public function allocate(Collection $orders, Collection $vehicles, array $options = []): array
42
+ {
43
+ $allowMulti = (bool) ($options['allow_multi_order'] ?? false);
44
+
45
+ // Sort orders: highest priority first, then earliest scheduled_at
46
+ $sorted = $orders->sortBy([
47
+ fn ($a, $b) => ($b->orchestrator_priority ?? 0) <=> ($a->orchestrator_priority ?? 0),
48
+ fn ($a, $b) => ($a->scheduled_at ?? now()) <=> ($b->scheduled_at ?? now()),
49
+ ])->values();
50
+
51
+ // Build a mutable list of available vehicles with their current location
52
+ $vehiclePool = $vehicles->map(function ($vehicle) {
53
+ $driver = $vehicle->driver;
54
+ $location = $driver?->location ?? $vehicle->location;
55
+
56
+ return [
57
+ 'model' => $vehicle,
58
+ 'id' => $vehicle->public_id,
59
+ 'driver_id' => $driver?->public_id,
60
+ 'lat' => $location?->getLat(),
61
+ 'lng' => $location?->getLng(),
62
+ 'assigned' => 0,
63
+ ];
64
+ })->values()->toArray();
65
+
66
+ $assignments = [];
67
+ $unassigned = [];
68
+
69
+ foreach ($sorted as $order) {
70
+ $pickupLat = $order->payload?->pickup?->lat ?? null;
71
+ $pickupLng = $order->payload?->pickup?->lng ?? null;
72
+
73
+ // Find the best vehicle: nearest with capacity remaining
74
+ $bestIdx = null;
75
+ $bestDist = PHP_INT_MAX;
76
+
77
+ foreach ($vehiclePool as $idx => $v) {
78
+ // In single-order mode, skip vehicles that already have an assignment
79
+ if (!$allowMulti && $v['assigned'] > 0) {
80
+ continue;
81
+ }
82
+
83
+ if ($v['lat'] === null || $v['lng'] === null) {
84
+ // Vehicle has no known location — eligible but deprioritised
85
+ if ($bestIdx === null) {
86
+ $bestIdx = $idx;
87
+ }
88
+ continue;
89
+ }
90
+
91
+ if ($pickupLat !== null && $pickupLng !== null) {
92
+ $dist = $this->haversineDistance($v['lat'], $v['lng'], (float) $pickupLat, (float) $pickupLng);
93
+ } else {
94
+ $dist = 0; // No pickup location — treat as zero distance
95
+ }
96
+
97
+ if ($dist < $bestDist) {
98
+ $bestDist = $dist;
99
+ $bestIdx = $idx;
100
+ }
101
+ }
102
+
103
+ if ($bestIdx === null) {
104
+ $unassigned[] = $order->public_id;
105
+ continue;
106
+ }
107
+
108
+ $v = $vehiclePool[$bestIdx];
109
+
110
+ $assignments[] = [
111
+ 'order_id' => $order->public_id,
112
+ 'vehicle_id' => $v['id'],
113
+ 'driver_id' => $v['driver_id'],
114
+ 'sequence' => ++$vehiclePool[$bestIdx]['assigned'],
115
+ 'arrival' => null,
116
+ 'duration' => null,
117
+ 'distance' => $bestDist < PHP_INT_MAX ? (int) round($bestDist) : null,
118
+ ];
119
+
120
+ // Update vehicle's "current position" to the order dropoff so the
121
+ // next order assigned to this vehicle is measured from the dropoff.
122
+ if ($allowMulti) {
123
+ $dropoffLat = $order->payload?->dropoff?->lat;
124
+ $dropoffLng = $order->payload?->dropoff?->lng;
125
+ if ($dropoffLat !== null && $dropoffLng !== null) {
126
+ $vehiclePool[$bestIdx]['lat'] = (float) $dropoffLat;
127
+ $vehiclePool[$bestIdx]['lng'] = (float) $dropoffLng;
128
+ }
129
+ }
130
+ }
131
+
132
+ return [
133
+ 'assignments' => $assignments,
134
+ 'unassigned' => $unassigned,
135
+ 'summary' => [
136
+ 'engine' => 'greedy',
137
+ 'assigned' => count($assignments),
138
+ 'unassigned' => count($unassigned),
139
+ ],
140
+ ];
141
+ }
142
+
143
+ /**
144
+ * Haversine distance in metres between two lat/lng points.
145
+ */
146
+ protected function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float
147
+ {
148
+ $R = 6371000;
149
+ $dLat = deg2rad($lat2 - $lat1);
150
+ $dLng = deg2rad($lng2 - $lng1);
151
+ $a = sin($dLat / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2;
152
+
153
+ return $R * 2 * atan2(sqrt($a), sqrt(1 - $a));
154
+ }
155
+ }
@@ -0,0 +1,272 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Orchestration\Engines;
4
+
5
+ use Illuminate\Support\Collection;
6
+
7
+ /**
8
+ * RouteSequencingEngine.
9
+ *
10
+ * Used for the `optimize_routes` orchestration mode.
11
+ *
12
+ * Unlike the allocation engines (Greedy, VROOM), this engine does NOT
13
+ * re-assign orders to vehicles. Instead it:
14
+ *
15
+ * 1. Groups the provided orders by their already-assigned vehicle
16
+ * (vehicle_assigned_uuid).
17
+ * 2. For each vehicle group, sequences the stops in an optimal order
18
+ * using a nearest-neighbour TSP heuristic.
19
+ * 3. Returns one assignment entry per order, preserving the existing
20
+ * vehicle_id / driver_id and updating only the sequence number.
21
+ *
22
+ * This ensures that running "Assign Vehicles" followed by "Optimize Routes"
23
+ * produces one properly-sequenced route per vehicle rather than re-running
24
+ * the allocation algorithm.
25
+ */
26
+ class RouteSequencingEngine
27
+ {
28
+ /**
29
+ * Sequence stops for each vehicle group.
30
+ *
31
+ * @param Collection $orders Orders with payload.pickup, payload.dropoff, payload.waypoints loaded
32
+ * @param array $options Engine options (currently unused, reserved for future use)
33
+ *
34
+ * @return array Standard orchestration result: { assignments, unassigned, summary }
35
+ */
36
+ public function sequence(Collection $orders, array $options = []): array
37
+ {
38
+ $assignments = [];
39
+ $unassigned = [];
40
+
41
+ // Group orders by their currently assigned vehicle UUID
42
+ $byVehicle = [];
43
+ foreach ($orders as $order) {
44
+ $vehicleUuid = $order->vehicle_assigned_uuid ?? null;
45
+ if (!$vehicleUuid) {
46
+ // Order has no vehicle assignment — cannot sequence it
47
+ $unassigned[] = $order->public_id;
48
+ continue;
49
+ }
50
+ $byVehicle[$vehicleUuid][] = $order;
51
+ }
52
+
53
+ foreach ($byVehicle as $vehicleUuid => $vehicleOrders) {
54
+ $vehicle = $vehicleOrders[0]->vehicle ?? null;
55
+
56
+ // Determine driver public_id from the vehicle relationship
57
+ $driverPublicId = null;
58
+ if ($vehicle && $vehicle->driver) {
59
+ $driverPublicId = $vehicle->driver->public_id ?? null;
60
+ }
61
+
62
+ // Get vehicle's current location as the starting point for sequencing
63
+ $startLat = null;
64
+ $startLng = null;
65
+ if ($vehicle) {
66
+ $driver = $vehicle->driver;
67
+ $location = $driver?->location ?? $vehicle->location;
68
+ $startLat = $location?->getLat();
69
+ $startLng = $location?->getLng();
70
+ }
71
+
72
+ // Build a flat list of stops: each order contributes pickup + dropoff
73
+ // (or waypoints for multi-drop orders). We keep pickup before its own
74
+ // dropoff as a hard constraint.
75
+ $sequenced = $this->_sequenceOrdersForVehicle($vehicleOrders, $startLat, $startLng);
76
+
77
+ // Build assignment entries — one per order, with the sequence number
78
+ // being the position of the order's FIRST stop in the sequenced list.
79
+ $orderSequences = [];
80
+ foreach ($sequenced as $seq => $stop) {
81
+ $orderId = $stop['order_public_id'];
82
+ // Only record the first occurrence (pickup position) per order
83
+ if (!isset($orderSequences[$orderId])) {
84
+ $orderSequences[$orderId] = $seq + 1; // 1-based
85
+ }
86
+ }
87
+
88
+ foreach ($vehicleOrders as $order) {
89
+ $assignments[] = [
90
+ 'order_id' => $order->public_id,
91
+ 'vehicle_id' => $vehicle?->public_id ?? $vehicleUuid,
92
+ 'driver_id' => $driverPublicId,
93
+ 'sequence' => $orderSequences[$order->public_id] ?? 1,
94
+ 'waypoint_sequence' => null,
95
+ 'arrival' => null,
96
+ 'duration' => null,
97
+ 'distance' => null,
98
+ ];
99
+ }
100
+ }
101
+
102
+ return [
103
+ 'assignments' => $assignments,
104
+ 'unassigned' => $unassigned,
105
+ 'summary' => [
106
+ 'engine' => 'route_sequencing',
107
+ 'assigned' => count($assignments),
108
+ 'unassigned' => count($unassigned),
109
+ ],
110
+ ];
111
+ }
112
+
113
+ /**
114
+ * Sequence stops for a single vehicle's orders using nearest-neighbour TSP.
115
+ *
116
+ * Constraints:
117
+ * - Each order's pickup must appear before its dropoff (precedence constraint).
118
+ * - Starts from the vehicle/driver's current location if known.
119
+ *
120
+ * @param array $orders Array of Order models
121
+ * @param float|null $startLat Vehicle starting latitude
122
+ * @param float|null $startLng Vehicle starting longitude
123
+ *
124
+ * @return array Ordered array of stop records: { order_public_id, lat, lng, type }
125
+ */
126
+ protected function _sequenceOrdersForVehicle(array $orders, ?float $startLat, ?float $startLng): array
127
+ {
128
+ // Build a flat pool of stops with precedence constraints
129
+ $pool = [];
130
+ foreach ($orders as $order) {
131
+ $payload = $order->payload;
132
+ if (!$payload) {
133
+ continue;
134
+ }
135
+
136
+ $waypoints = $payload->waypoints;
137
+ $hasWaypoints = $waypoints && $waypoints->count() > 0;
138
+ $isMultiDrop = $hasWaypoints && !$payload->pickup_uuid && !$payload->dropoff_uuid;
139
+
140
+ if ($isMultiDrop) {
141
+ // Multi-drop: add each waypoint as a stop
142
+ $sorted = $waypoints->sortBy('order')->values();
143
+ foreach ($sorted as $idx => $wp) {
144
+ $place = $wp->place;
145
+ if (!$place) {
146
+ continue;
147
+ }
148
+ $pool[] = [
149
+ 'order_public_id' => $order->public_id,
150
+ 'lat' => (float) ($place->lat ?? 0),
151
+ 'lng' => (float) ($place->lng ?? 0),
152
+ 'type' => 'waypoint',
153
+ 'precedence_after' => $idx > 0 ? ($pool[count($pool) - 1]['id'] ?? null) : null,
154
+ 'id' => $order->public_id . '_wp_' . $idx,
155
+ ];
156
+ }
157
+ } else {
158
+ // Standard pickup → dropoff
159
+ $pickup = $payload->pickup;
160
+ $dropoff = $payload->dropoff;
161
+
162
+ $pickupId = $order->public_id . '_pickup';
163
+ $dropoffId = $order->public_id . '_dropoff';
164
+
165
+ if ($pickup && $pickup->lat && $pickup->lng) {
166
+ $pool[] = [
167
+ 'order_public_id' => $order->public_id,
168
+ 'lat' => (float) $pickup->lat,
169
+ 'lng' => (float) $pickup->lng,
170
+ 'type' => 'pickup',
171
+ 'precedence_after' => null, // pickup has no prerequisite
172
+ 'id' => $pickupId,
173
+ 'blocks' => $dropoffId, // dropoff must come after this
174
+ ];
175
+ }
176
+
177
+ if ($dropoff && $dropoff->lat && $dropoff->lng) {
178
+ $pool[] = [
179
+ 'order_public_id' => $order->public_id,
180
+ 'lat' => (float) $dropoff->lat,
181
+ 'lng' => (float) $dropoff->lng,
182
+ 'type' => 'dropoff',
183
+ 'precedence_after' => $pickupId, // must come after pickup
184
+ 'id' => $dropoffId,
185
+ ];
186
+ }
187
+ }
188
+ }
189
+
190
+ if (empty($pool)) {
191
+ return [];
192
+ }
193
+
194
+ // Nearest-neighbour TSP with precedence constraints
195
+ $visited = [];
196
+ $sequence = [];
197
+ $curLat = $startLat;
198
+ $curLng = $startLng;
199
+
200
+ while (count($visited) < count($pool)) {
201
+ $bestIdx = null;
202
+ $bestDist = PHP_INT_MAX;
203
+
204
+ foreach ($pool as $idx => $stop) {
205
+ if (in_array($idx, $visited)) {
206
+ continue;
207
+ }
208
+
209
+ // Check precedence: if this stop requires another stop to come first
210
+ if (!empty($stop['precedence_after'])) {
211
+ $prerequisiteId = $stop['precedence_after'];
212
+ $prerequisiteDone = false;
213
+ foreach ($sequence as $done) {
214
+ if ($done['id'] === $prerequisiteId) {
215
+ $prerequisiteDone = true;
216
+ break;
217
+ }
218
+ }
219
+ if (!$prerequisiteDone) {
220
+ continue;
221
+ } // not yet eligible
222
+ }
223
+
224
+ if ($curLat === null || $curLng === null) {
225
+ // No current position — just pick the first eligible stop
226
+ $bestIdx = $idx;
227
+ break;
228
+ }
229
+
230
+ $dist = $this->_haversine($curLat, $curLng, $stop['lat'], $stop['lng']);
231
+ if ($dist < $bestDist) {
232
+ $bestDist = $dist;
233
+ $bestIdx = $idx;
234
+ }
235
+ }
236
+
237
+ if ($bestIdx === null) {
238
+ // No eligible stop found — add all remaining stops in original order
239
+ // (safety fallback to avoid infinite loop)
240
+ foreach ($pool as $idx => $stop) {
241
+ if (!in_array($idx, $visited)) {
242
+ $visited[] = $idx;
243
+ $sequence[] = $stop;
244
+ $curLat = $stop['lat'];
245
+ $curLng = $stop['lng'];
246
+ }
247
+ }
248
+ break;
249
+ }
250
+
251
+ $visited[] = $bestIdx;
252
+ $sequence[] = $pool[$bestIdx];
253
+ $curLat = $pool[$bestIdx]['lat'];
254
+ $curLng = $pool[$bestIdx]['lng'];
255
+ }
256
+
257
+ return $sequence;
258
+ }
259
+
260
+ /**
261
+ * Haversine distance in metres.
262
+ */
263
+ protected function _haversine(float $lat1, float $lng1, float $lat2, float $lng2): float
264
+ {
265
+ $R = 6371000;
266
+ $dLat = deg2rad($lat2 - $lat1);
267
+ $dLng = deg2rad($lng2 - $lng1);
268
+ $a = sin($dLat / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2;
269
+
270
+ return $R * 2 * atan2(sqrt($a), sqrt(1 - $a));
271
+ }
272
+ }