@fleetbase/fleetops-engine 0.6.38 → 0.6.39

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 (429) 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 +40 -36
  25. package/addon/components/layout/fleet-ops-sidebar.js +61 -10
  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 +1087 -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 +30 -0
  117. package/addon/components/vehicle/form.hbs +39 -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 +80 -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 +5 -3
  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 +58 -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 +41 -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/resources/views/mail/maintenance-schedule-reminder.blade.php +59 -0
  358. package/server/resources/views/mail/work-order-dispatched.blade.php +67 -0
  359. package/server/src/Auth/Schemas/FleetOps.php +44 -0
  360. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +150 -0
  361. package/server/src/Console/Commands/SendMaintenanceReminders.php +128 -0
  362. package/server/src/Http/Controllers/Internal/v1/DriverController.php +1 -0
  363. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +27 -0
  364. package/server/src/Http/Controllers/Internal/v1/LiveController.php +9 -2
  365. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +165 -0
  366. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +304 -0
  367. package/server/src/Http/Controllers/Internal/v1/ManifestController.php +138 -0
  368. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +975 -0
  369. package/server/src/Http/Controllers/Internal/v1/OrderController.php +42 -0
  370. package/server/src/Http/Controllers/Internal/v1/PartController.php +27 -0
  371. package/server/src/Http/Controllers/Internal/v1/SettingController.php +118 -0
  372. package/server/src/Http/Controllers/Internal/v1/Traits/DriverSchedulingTrait.php +214 -0
  373. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +68 -0
  374. package/server/src/Http/Resources/v1/Driver.php +1 -0
  375. package/server/src/Http/Resources/v1/Maintenance.php +138 -0
  376. package/server/src/Http/Resources/v1/MaintenanceSchedule.php +137 -0
  377. package/server/src/Http/Resources/v1/Orchestrator/Order.php +116 -0
  378. package/server/src/Http/Resources/v1/Order.php +7 -4
  379. package/server/src/Http/Resources/v1/Waypoint.php +7 -0
  380. package/server/src/Http/Resources/v1/WorkOrder.php +136 -0
  381. package/server/src/Imports/EquipmentImport.php +32 -0
  382. package/server/src/Imports/MaintenanceImport.php +32 -0
  383. package/server/src/Imports/MaintenanceScheduleImport.php +32 -0
  384. package/server/src/Imports/PartImport.php +32 -0
  385. package/server/src/Imports/WorkOrderImport.php +32 -0
  386. package/server/src/Jobs/ProcessAllocationJob.php +119 -0
  387. package/server/src/Listeners/HandleDeliveryCompletion.php +47 -0
  388. package/server/src/Listeners/NotifyDriverOnShiftChange.php +63 -0
  389. package/server/src/Mail/MaintenanceScheduleReminder.php +68 -0
  390. package/server/src/Mail/WorkOrderDispatched.php +58 -0
  391. package/server/src/Models/Asset.php +2 -2
  392. package/server/src/Models/Device.php +1 -1
  393. package/server/src/Models/Driver.php +82 -4
  394. package/server/src/Models/Equipment.php +62 -2
  395. package/server/src/Models/Maintenance.php +127 -9
  396. package/server/src/Models/MaintenanceSchedule.php +353 -0
  397. package/server/src/Models/Manifest.php +214 -0
  398. package/server/src/Models/ManifestStop.php +162 -0
  399. package/server/src/Models/Order.php +70 -0
  400. package/server/src/Models/OrderConfig.php +5 -2
  401. package/server/src/Models/Part.php +69 -3
  402. package/server/src/Models/Payload.php +7 -2
  403. package/server/src/Models/Place.php +1 -1
  404. package/server/src/Models/Sensor.php +1 -1
  405. package/server/src/Models/ServiceQuote.php +1 -1
  406. package/server/src/Models/Vehicle.php +20 -1
  407. package/server/src/Models/Warranty.php +1 -1
  408. package/server/src/Models/Waypoint.php +7 -1
  409. package/server/src/Models/WorkOrder.php +122 -12
  410. package/server/src/Notifications/DriverShiftChanged.php +110 -0
  411. package/server/src/Observers/WorkOrderObserver.php +107 -0
  412. package/server/src/Orchestration/Contracts/OrchestrationEngineInterface.php +63 -0
  413. package/server/src/Orchestration/Engines/DriverAssignmentEngine.php +265 -0
  414. package/server/src/Orchestration/Engines/GreedyOrchestrationEngine.php +155 -0
  415. package/server/src/Orchestration/Engines/RouteSequencingEngine.php +272 -0
  416. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +192 -0
  417. package/server/src/Orchestration/OrchestrationEngineRegistry.php +83 -0
  418. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +290 -0
  419. package/server/src/Providers/EventServiceProvider.php +7 -1
  420. package/server/src/Providers/FleetOpsServiceProvider.php +42 -15
  421. package/server/src/routes.php +65 -4
  422. package/translations/ar-ae.yml +44 -12
  423. package/translations/bg-bg.yaml +51 -10
  424. package/translations/en-us.yaml +444 -1
  425. package/translations/fr-fr.yaml +51 -10
  426. package/translations/mn-mn.yaml +51 -10
  427. package/translations/pt-br.yaml +51 -10
  428. package/translations/ru-ru.yaml +51 -10
  429. package/translations/vi-vn.yaml +48 -12
@@ -0,0 +1,975 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
4
+
5
+ use Fleetbase\FleetOps\Http\Resources\v1\Orchestrator\Order as OrchestratorOrderResource;
6
+ use Fleetbase\FleetOps\Models\Contact;
7
+ use Fleetbase\FleetOps\Models\Driver;
8
+ use Fleetbase\FleetOps\Models\Manifest;
9
+ use Fleetbase\FleetOps\Models\ManifestStop;
10
+ use Fleetbase\FleetOps\Models\Order;
11
+ use Fleetbase\FleetOps\Models\OrderConfig;
12
+ use Fleetbase\FleetOps\Models\Payload;
13
+ use Fleetbase\FleetOps\Models\Place;
14
+ use Fleetbase\FleetOps\Models\Vehicle;
15
+ use Fleetbase\FleetOps\Models\Vendor;
16
+ use Fleetbase\FleetOps\Orchestration\Engines\DriverAssignmentEngine;
17
+ use Fleetbase\FleetOps\Orchestration\Engines\RouteSequencingEngine;
18
+ use Fleetbase\FleetOps\Orchestration\OrchestrationEngineRegistry;
19
+ use Fleetbase\Http\Controllers\Controller;
20
+ use Fleetbase\Models\Setting;
21
+ use Illuminate\Http\JsonResponse;
22
+ use Illuminate\Http\Request;
23
+ use Illuminate\Support\Carbon;
24
+ use Illuminate\Support\Facades\DB;
25
+ use Illuminate\Support\Str;
26
+
27
+ /**
28
+ * OrchestrationController.
29
+ *
30
+ * HTTP interface for the Orchestrator Workbench.
31
+ *
32
+ * Responsibilities:
33
+ * - Serving orders for the workbench (with custom field values)
34
+ * - Running orchestration phases (assign_vehicles, assign_drivers, optimize, optimize_routes, allocate)
35
+ * - Committing a proposed plan to Manifests and ManifestStops
36
+ * - Listing available orchestration engines
37
+ * - Providing order-config custom field definitions for card configuration
38
+ * - Importing orders from parsed CSV/Excel data
39
+ */
40
+ class OrchestrationController extends Controller
41
+ {
42
+ public function __construct(protected OrchestrationEngineRegistry $registry)
43
+ {
44
+ }
45
+
46
+ /**
47
+ * Return orders for the Orchestrator Workbench.
48
+ *
49
+ * This endpoint uses the dedicated OrchestratorOrderResource which includes
50
+ * custom_field_values — unlike the lightweight Index/Order resource used by
51
+ * the tabular orders view, which intentionally omits them for performance.
52
+ *
53
+ * GET /int/v1/fleet-ops/orchestrator/orders
54
+ */
55
+ public function orders(Request $request): JsonResponse
56
+ {
57
+ $companyUuid = session('company');
58
+
59
+ $query = Order::where('company_uuid', $companyUuid)->whereIn('status', ['created', 'dispatched', 'started']);
60
+
61
+ $query->whereHas('payload', function ($payloadQuery) {
62
+ $payloadQuery->where(function ($q) {
63
+ $q->whereHas('waypoints', function ($w) {
64
+ $w->whereNotNull('waypoints.uuid');
65
+ });
66
+ $q->orWhereHas('pickup', function ($p) {
67
+ $p->whereNotNull('places.uuid');
68
+ });
69
+ $q->orWhereHas('dropoff', function ($d) {
70
+ $d->whereNotNull('places.uuid');
71
+ });
72
+ });
73
+ });
74
+
75
+ $query->whereHas('trackingNumber', function ($q) {
76
+ $q->select('uuid');
77
+ });
78
+
79
+ $query->whereHas('trackingStatuses', function ($q) {
80
+ $q->select('uuid');
81
+ });
82
+
83
+ if ($request->boolean('unassigned')) {
84
+ $query->whereNull('vehicle_assigned_uuid');
85
+ }
86
+
87
+ $query->with([
88
+ 'payload.entities',
89
+ 'payload.waypoints',
90
+ 'payload.pickup',
91
+ 'payload.dropoff',
92
+ 'payload.return',
93
+ 'trackingNumber',
94
+ 'trackingStatuses',
95
+ 'driverAssigned' => function ($query) {
96
+ $query->without(['jobs', 'currentJob']);
97
+ },
98
+ 'vehicleAssigned' => function ($query) {
99
+ $query->without(['fleets', 'vendor']);
100
+ },
101
+ 'customer',
102
+ 'facilitator',
103
+ 'customFieldValues.customField',
104
+ ]);
105
+
106
+ $limit = min((int) $request->input('limit', 500), 1000);
107
+ $orders = $query->limit($limit)->get();
108
+
109
+ return response()->json([
110
+ 'orders' => OrchestratorOrderResource::collection($orders)->resolve(),
111
+ ]);
112
+ }
113
+
114
+ /**
115
+ * Run an orchestration phase for the given mode.
116
+ *
117
+ * POST /int/v1/fleet-ops/orchestrator/run
118
+ */
119
+ public function run(Request $request): JsonResponse
120
+ {
121
+ $companyUuid = session('company');
122
+ $mode = $request->input('mode', 'assign_vehicles');
123
+ $orderIds = $request->input('order_ids', []);
124
+ $vehicleIds = $request->input('vehicle_ids', []);
125
+ $driverIds = $request->input('driver_ids', []);
126
+ $options = $request->input('options', []);
127
+ // prior_assignments: assignments from previous phases that have not yet
128
+ // been committed to the database. Keyed by order_id (public_id).
129
+ $priorAssignments = collect($request->input('prior_assignments', []))
130
+ ->keyBy('order_id');
131
+
132
+ // ── Resolve orders ────────────────────────────────────────────────────
133
+ $ordersQuery = Order::where('company_uuid', $companyUuid)
134
+ ->whereIn('status', ['created', 'dispatched', 'started'])
135
+ ->with(['payload.dropoff', 'payload.pickup', 'payload.waypoints', 'payload.waypointMarkers', 'payload.entities']);
136
+
137
+ if ($mode === 'assign_vehicles' || $mode === 'allocate') {
138
+ // Exclude orders that already have a vehicle assigned in the DB
139
+ // OR in a prior uncommitted phase.
140
+ $priorVehicleAssignedOrderIds = $priorAssignments
141
+ ->filter(fn ($a) => !empty($a['vehicle_id']))
142
+ ->keys()
143
+ ->toArray();
144
+ $ordersQuery->whereNull('vehicle_assigned_uuid');
145
+ if (!empty($priorVehicleAssignedOrderIds)) {
146
+ $ordersQuery->whereNotIn('public_id', $priorVehicleAssignedOrderIds);
147
+ }
148
+ } elseif ($mode === 'optimize') {
149
+ $ordersQuery->whereNotNull('vehicle_assigned_uuid');
150
+ } elseif ($mode === 'optimize_routes') {
151
+ // optimize_routes: re-sequence stops for selected orders.
152
+ // No vehicle-assignment filter — the user picks the orders explicitly.
153
+ } elseif ($mode === 'assign_drivers') {
154
+ // For assign_drivers we need orders that have a vehicle assigned
155
+ // (either committed to DB or from a prior phase) but no driver yet.
156
+ $priorVehicleAssignedOrderIds = $priorAssignments
157
+ ->filter(fn ($a) => !empty($a['vehicle_id']))
158
+ ->keys()
159
+ ->toArray();
160
+
161
+ if (!empty($priorVehicleAssignedOrderIds)) {
162
+ // Use the prior phase's vehicle assignments — fetch those orders
163
+ // regardless of their DB vehicle_assigned_uuid.
164
+ $ordersQuery->whereIn('public_id', $priorVehicleAssignedOrderIds)
165
+ ->whereNull('driver_assigned_uuid');
166
+ } else {
167
+ // Standalone assign_drivers (no prior vehicle phase):
168
+ // Use all selected orders regardless of vehicle assignment.
169
+ // The engine will assign both a vehicle and a driver together.
170
+ $ordersQuery->whereNull('driver_assigned_uuid');
171
+ }
172
+ }
173
+
174
+ if (!empty($orderIds)) {
175
+ $ordersQuery->whereIn('public_id', $orderIds);
176
+ }
177
+ $orders = $ordersQuery->get();
178
+
179
+ // Augment orders with prior-phase vehicle AND driver assignments so the
180
+ // engines can group by vehicle_id and preserve driver_id even before the
181
+ // plan is committed to the database.
182
+ if ($priorAssignments->isNotEmpty()) {
183
+ // Pre-load all drivers referenced in prior assignments so we can
184
+ // attach them to vehicles without N+1 queries.
185
+ $priorDriverIds = $priorAssignments
186
+ ->pluck('driver_id')
187
+ ->filter()
188
+ ->unique()
189
+ ->values()
190
+ ->toArray();
191
+ $priorDriverMap = collect();
192
+ if (!empty($priorDriverIds)) {
193
+ $priorDriverMap = Driver::whereIn('public_id', $priorDriverIds)
194
+ ->get()
195
+ ->keyBy('public_id');
196
+ }
197
+
198
+ foreach ($orders as $order) {
199
+ $prior = $priorAssignments->get($order->public_id);
200
+ if (!$prior) {
201
+ continue;
202
+ }
203
+
204
+ // Temporarily set the vehicle_assigned_uuid on the model
205
+ // so engines that group by this field work correctly.
206
+ if (!empty($prior['vehicle_id']) && !$order->vehicle_assigned_uuid) {
207
+ // Resolve the Vehicle model and attach it
208
+ $vehicle = Vehicle::where('public_id', $prior['vehicle_id'])
209
+ ->with(['driver' => fn ($q) => $q->with(['scheduleItems'])])
210
+ ->first();
211
+ if ($vehicle) {
212
+ $order->vehicle_assigned_uuid = $vehicle->uuid;
213
+
214
+ // If the prior phase assigned a driver that is not yet
215
+ // linked to this vehicle in the DB, attach that driver
216
+ // to the vehicle relation so RouteSequencingEngine (and
217
+ // any other engine) can read $vehicle->driver correctly.
218
+ if (!empty($prior['driver_id'])) {
219
+ $priorDriver = $priorDriverMap->get($prior['driver_id']);
220
+ if ($priorDriver && (!$vehicle->driver || $vehicle->driver->public_id !== $prior['driver_id'])) {
221
+ $vehicle->setRelation('driver', $priorDriver);
222
+ }
223
+ }
224
+
225
+ $order->setRelation('vehicle', $vehicle);
226
+ }
227
+ }
228
+
229
+ // Also temporarily set driver_assigned_uuid so engines that
230
+ // check this field (e.g. for deduplication) see the prior assignment.
231
+ if (!empty($prior['driver_id']) && !$order->driver_assigned_uuid) {
232
+ $priorDriver = $priorDriverMap->get($prior['driver_id']);
233
+ if ($priorDriver) {
234
+ $order->driver_assigned_uuid = $priorDriver->uuid;
235
+ $order->setRelation('driverAssigned', $priorDriver);
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ // ── Resolve vehicles ──────────────────────────────────────────────────
242
+ $vehiclesQuery = Vehicle::where('company_uuid', $companyUuid)
243
+ ->with(['driver' => fn ($q) => $q->with(['scheduleItems'])]);
244
+
245
+ if (!empty($vehicleIds)) {
246
+ $vehiclesQuery->whereIn('public_id', $vehicleIds);
247
+ } elseif (!empty($driverIds)) {
248
+ $vehiclesQuery->whereHas('driver', fn ($q) => $q->whereIn('public_id', $driverIds));
249
+ }
250
+
251
+ // assign_vehicles, assign_drivers and optimize_routes do not require an
252
+ // online/assigned driver — use all matching vehicles as-is.
253
+ if (in_array($mode, ['assign_vehicles', 'assign_drivers', 'optimize_routes'])) {
254
+ $vehicles = $vehiclesQuery->get();
255
+ } else {
256
+ // Legacy allocate / optimize modes require a driver to be linked.
257
+ $vehicles = $vehiclesQuery->get()->filter(fn ($v) => $v->driver !== null);
258
+ }
259
+
260
+ if ($orders->isEmpty()) {
261
+ return response()->json([
262
+ 'message' => 'No orders found for the given criteria.',
263
+ 'assignments' => [],
264
+ 'unassigned' => [],
265
+ ], 200);
266
+ }
267
+
268
+ if ($vehicles->isEmpty() && $mode !== 'assign_drivers') {
269
+ return response()->json([
270
+ 'message' => 'No available vehicles found.',
271
+ 'assignments' => [],
272
+ 'unassigned' => $orders->pluck('public_id'),
273
+ ], 200);
274
+ }
275
+
276
+ // ── Run engine ────────────────────────────────────────────────────────
277
+ $engineId = $mode === 'assign_drivers'
278
+ ? 'driver_assignment'
279
+ : ($request->input('options.engine') ?? Setting::lookup('fleetops.orchestrator_engine', 'greedy'));
280
+
281
+ try {
282
+ if ($mode === 'assign_drivers') {
283
+ $engine = new DriverAssignmentEngine();
284
+ $result = $engine->assign($orders, $vehicles, $options);
285
+ } elseif ($mode === 'optimize_routes') {
286
+ // optimize_routes sequences stops within each vehicle's already-assigned
287
+ // order group — it does NOT re-assign orders to different vehicles.
288
+ //
289
+ // IMPORTANT: Do NOT call $orders->load(['vehicle', 'vehicle.driver']) here.
290
+ // The augmentation loop above already called setRelation('vehicle', $vehicle)
291
+ // with the prior-phase driver attached via setRelation('driver', $priorDriver).
292
+ // Calling ->load() would reload from the DB and OVERWRITE those in-memory
293
+ // relations, losing the uncommitted driver assignment from a prior phase.
294
+ //
295
+ // Instead, only load the vehicle relation for orders that have a
296
+ // vehicle_assigned_uuid in the DB but no in-memory relation set yet
297
+ // (i.e. standalone optimize_routes without a prior assign_drivers phase).
298
+ foreach ($orders as $order) {
299
+ if (!$order->relationLoaded('vehicle') && $order->vehicle_assigned_uuid) {
300
+ $vehicle = Vehicle::where('uuid', $order->vehicle_assigned_uuid)
301
+ ->with(['driver'])
302
+ ->first();
303
+ if ($vehicle) {
304
+ $order->setRelation('vehicle', $vehicle);
305
+ }
306
+ }
307
+ }
308
+ $engine = new RouteSequencingEngine();
309
+ $result = $engine->sequence($orders, $options);
310
+ } else {
311
+ $engine = $this->registry->resolve($engineId);
312
+ $result = $engine->allocate($orders, $vehicles, $options);
313
+ }
314
+ } catch (\RuntimeException $e) {
315
+ // Engine is unavailable (e.g. VROOM not reachable).
316
+ // Return a structured JSON 503 so the frontend can display a
317
+ // user-friendly message instead of an unhandled exception page.
318
+ return response()->json([
319
+ 'error' => $e->getMessage(),
320
+ 'hint' => 'If you are using the VROOM engine, ensure the VROOM service is running and VROOM_HOST is configured correctly. Alternatively, switch to the built-in "greedy" engine in Orchestrator Settings.',
321
+ 'engine' => $engineId,
322
+ ], 503);
323
+ }
324
+
325
+ return response()->json($result);
326
+ }
327
+
328
+ /**
329
+ * Preview an orchestration run without committing any assignments.
330
+ *
331
+ * GET /int/v1/fleet-ops/orchestrator/preview
332
+ */
333
+ public function preview(Request $request): JsonResponse
334
+ {
335
+ return $this->run($request);
336
+ }
337
+
338
+ /**
339
+ * Commit an orchestration plan — creates Manifests and ManifestStops.
340
+ *
341
+ * Does NOT trigger dispatch or update order status. That is the
342
+ * responsibility of the operational flow (driver actions / dispatcher).
343
+ *
344
+ * POST /int/v1/fleet-ops/orchestrator/commit
345
+ */
346
+ public function commit(Request $request): JsonResponse
347
+ {
348
+ $assignments = $request->input('assignments', []);
349
+ $scheduledDate = $request->input('scheduled_date', now()->toDateString());
350
+ $companyUuid = session('company');
351
+
352
+ if (empty($assignments)) {
353
+ return response()->json(['error' => 'No assignments provided.'], 422);
354
+ }
355
+
356
+ $committed = [];
357
+ $failed = [];
358
+ $manifests = [];
359
+
360
+ DB::beginTransaction();
361
+ try {
362
+ // Group assignments by vehicle_id
363
+ $byVehicle = [];
364
+ foreach ($assignments as $assignment) {
365
+ $vehicleId = $assignment['vehicle_id'] ?? null;
366
+ if (!$vehicleId) {
367
+ $failed[] = $assignment['order_id'] ?? 'unknown';
368
+ continue;
369
+ }
370
+ $byVehicle[$vehicleId][] = $assignment;
371
+ }
372
+
373
+ foreach ($byVehicle as $vehiclePublicId => $vehicleAssignments) {
374
+ $vehicle = Vehicle::where('public_id', $vehiclePublicId)->first();
375
+ if (!$vehicle) {
376
+ foreach ($vehicleAssignments as $a) {
377
+ $failed[] = $a['order_id'];
378
+ }
379
+ continue;
380
+ }
381
+
382
+ // Driver is optional (vehicle-only assignment)
383
+ $driverPublicId = $vehicleAssignments[0]['driver_id'] ?? null;
384
+ $driver = $driverPublicId
385
+ ? Driver::where('public_id', $driverPublicId)->first()
386
+ : null;
387
+
388
+ $totalDistance = (int) array_sum(array_column($vehicleAssignments, 'distance'));
389
+ $totalDuration = (int) array_sum(array_column($vehicleAssignments, 'duration'));
390
+
391
+ // Create Manifest
392
+ $manifest = Manifest::create([
393
+ 'company_uuid' => $companyUuid,
394
+ 'vehicle_uuid' => $vehicle->uuid,
395
+ 'driver_uuid' => $driver?->uuid,
396
+ 'status' => 'draft',
397
+ 'scheduled_date' => $scheduledDate,
398
+ 'total_distance_m' => $totalDistance,
399
+ 'total_duration_s' => $totalDuration,
400
+ 'stop_count' => count($vehicleAssignments),
401
+ ]);
402
+
403
+ // Sort stops by sequence
404
+ usort($vehicleAssignments, fn ($a, $b) => ($a['sequence'] ?? 0) <=> ($b['sequence'] ?? 0));
405
+
406
+ foreach ($vehicleAssignments as $idx => $assignment) {
407
+ $order = Order::where('public_id', $assignment['order_id'])->first();
408
+ if (!$order) {
409
+ $failed[] = $assignment['order_id'];
410
+ continue;
411
+ }
412
+
413
+ $placeUuid = $order->payload?->dropoff?->uuid ?? null;
414
+
415
+ ManifestStop::create([
416
+ 'manifest_uuid' => $manifest->uuid,
417
+ 'order_uuid' => $order->uuid,
418
+ 'place_uuid' => $placeUuid,
419
+ 'sequence' => (int) ($assignment['sequence'] ?? ($idx + 1)),
420
+ 'status' => 'pending',
421
+ 'estimated_arrival' => isset($assignment['arrival'])
422
+ ? Carbon::createFromTimestamp($assignment['arrival'])
423
+ : null,
424
+ 'distance_from_prev_m' => (int) ($assignment['distance'] ?? 0),
425
+ 'duration_from_prev_s' => (int) ($assignment['duration'] ?? 0),
426
+ ]);
427
+
428
+ // Update order assignments
429
+ $order->vehicle_assigned_uuid = $vehicle->uuid;
430
+ $order->manifest_uuid = $manifest->uuid;
431
+ if ($driver) {
432
+ $order->driver_assigned_uuid = $driver->uuid;
433
+ }
434
+ if (isset($assignment['sequence'])) {
435
+ $order->is_route_optimized = true;
436
+ }
437
+ $order->save();
438
+
439
+ // Update waypoint sequence if provided
440
+ if (!empty($assignment['waypoint_sequence']) && $order->payload) {
441
+ foreach ($assignment['waypoint_sequence'] as $seq => $waypointId) {
442
+ DB::table('waypoints')
443
+ ->where('payload_uuid', $order->payload_uuid)
444
+ ->where('public_id', $waypointId)
445
+ ->update(['order' => $seq]);
446
+ }
447
+ }
448
+
449
+ $committed[] = $assignment['order_id'];
450
+ }
451
+
452
+ $manifests[] = $manifest->public_id;
453
+ }
454
+
455
+ DB::commit();
456
+ } catch (\Exception $e) {
457
+ // Only roll back if a transaction is still active.
458
+ // A PDOException from a missing table can cause MySQL to implicitly
459
+ // roll back the transaction before we reach this catch block.
460
+ if (DB::transactionLevel() > 0) {
461
+ DB::rollBack();
462
+ }
463
+
464
+ return response()->json(['error' => 'Commit failed: ' . $e->getMessage()], 500);
465
+ }
466
+
467
+ return response()->json([
468
+ 'committed' => $committed,
469
+ 'failed' => $failed,
470
+ 'manifests' => $manifests,
471
+ ]);
472
+ }
473
+
474
+ /**
475
+ * Return available orchestration engines.
476
+ *
477
+ * GET /int/v1/fleet-ops/orchestrator/engines
478
+ */
479
+ public function engines(): JsonResponse
480
+ {
481
+ return response()->json([
482
+ 'engines' => $this->registry->available(),
483
+ ]);
484
+ }
485
+
486
+ /**
487
+ * Return all active Order Configs with their custom field definitions.
488
+ * Used by the Orchestrator Settings UI for configurable card fields.
489
+ *
490
+ * GET /int/v1/fleet-ops/orchestrator/order-config-fields
491
+ */
492
+ public function orderConfigFields(): JsonResponse
493
+ {
494
+ $companyUuid = session('company');
495
+
496
+ $configs = OrderConfig::where('company_uuid', $companyUuid)
497
+ ->with('customFields')
498
+ ->get(['uuid', 'public_id', 'name', 'key'])
499
+ ->map(function ($config) {
500
+ // customFields is a morphMany on subject_uuid/subject_type.
501
+ // If the eager load returned nothing (e.g. subject_type mismatch),
502
+ // fall back to a direct query by subject_uuid.
503
+ $customFields = $config->customFields;
504
+ if ($customFields->isEmpty()) {
505
+ $customFields = \Fleetbase\Models\CustomField::where('subject_uuid', $config->uuid)
506
+ ->orderBy('order')
507
+ ->get();
508
+ }
509
+
510
+ $fields = $customFields
511
+ ->map(fn ($field) => [
512
+ 'key' => $field->name ?? Str::slug($field->label ?? '', '_'),
513
+ 'label' => $field->label ?? $field->name ?? '',
514
+ 'type' => $field->type ?? 'text',
515
+ 'required' => (bool) ($field->required ?? false),
516
+ ])
517
+ ->values();
518
+
519
+ return [
520
+ 'id' => $config->public_id,
521
+ 'uuid' => $config->uuid,
522
+ 'name' => $config->name,
523
+ 'key' => $config->key,
524
+ 'fields' => $fields,
525
+ ];
526
+ });
527
+
528
+ // Exclude configs that have no custom fields at all
529
+ $configs = $configs->filter(fn ($config) => count($config['fields']) > 0)->values();
530
+
531
+ return response()->json(['configs' => $configs]);
532
+ }
533
+
534
+ /**
535
+ * Import orders from parsed CSV/Excel row data.
536
+ *
537
+ * POST /int/v1/fleet-ops/orchestrator/import-orders
538
+ */
539
+ public function importOrders(Request $request): JsonResponse
540
+ {
541
+ $rows = $request->input('rows', []);
542
+ $companyUuid = session('company');
543
+
544
+ if (empty($rows)) {
545
+ return response()->json(['error' => 'No rows provided.'], 422);
546
+ }
547
+
548
+ $created = [];
549
+ $failed = [];
550
+
551
+ // ── Group multi-waypoint rows by order_ref ────────────────────────────
552
+ // Rows with order_type = 'multi_waypoint' and the same order_ref are
553
+ // collapsed into a single order where each row becomes one waypoint.
554
+ $groups = [];
555
+ foreach ($rows as $row) {
556
+ $orderType = strtolower(trim($row['order_type'] ?? 'pickup_dropoff'));
557
+ $orderRef = trim($row['order_ref'] ?? '');
558
+
559
+ if ($orderType === 'multi_waypoint' && $orderRef !== '') {
560
+ $groups[$orderRef][] = $row;
561
+ } else {
562
+ // Each pickup/dropoff row is its own independent group.
563
+ $groups['__single_' . Str::uuid()][] = $row;
564
+ }
565
+ }
566
+
567
+ foreach ($groups as $groupKey => $groupRows) {
568
+ DB::beginTransaction();
569
+ try {
570
+ // Use the first row for order-level metadata.
571
+ $firstRow = $groupRows[0];
572
+ $orderType = strtolower(trim($firstRow['order_type'] ?? 'pickup_dropoff'));
573
+ $isMulti = $orderType === 'multi_waypoint';
574
+
575
+ // ── Resolve OrderConfig ───────────────────────────────────────
576
+ $orderConfigUuid = null;
577
+ if (!empty($firstRow['type'])) {
578
+ $orderConfig = OrderConfig::resolveFromIdentifier($firstRow['type']);
579
+ if ($orderConfig) {
580
+ $orderConfigUuid = $orderConfig->uuid;
581
+ }
582
+ }
583
+
584
+ // ── Resolve Customer ─────────────────────────────────────────
585
+ $customerUuid = null;
586
+ $customerType = null;
587
+ if (!empty($firstRow['customer_email']) || !empty($firstRow['customer_phone']) || !empty($firstRow['customer_name'])) {
588
+ $customerEntityType = strtolower(trim($firstRow['customer_type'] ?? 'contact'));
589
+ if ($customerEntityType === 'vendor') {
590
+ $vendor = $this->resolveOrCreateVendor($firstRow, $companyUuid, 'customer');
591
+ if ($vendor) {
592
+ $customerUuid = $vendor->uuid;
593
+ $customerType = 'Fleetbase\\FleetOps\\Models\\Vendor';
594
+ }
595
+ } else {
596
+ $contact = $this->resolveOrCreateContact($firstRow, $companyUuid, 'customer');
597
+ if ($contact) {
598
+ $customerUuid = $contact->uuid;
599
+ $customerType = 'Fleetbase\\FleetOps\\Models\\Contact';
600
+ }
601
+ }
602
+ }
603
+
604
+ // ── Resolve Facilitator ───────────────────────────────────────
605
+ $facilitatorUuid = null;
606
+ $facilitatorType = null;
607
+ if (!empty($firstRow['facilitator_email']) || !empty($firstRow['facilitator_phone']) || !empty($firstRow['facilitator_name'])) {
608
+ $facilitatorEntityType = strtolower(trim($firstRow['facilitator_type'] ?? 'vendor'));
609
+ if ($facilitatorEntityType === 'contact') {
610
+ $contact = $this->resolveOrCreateContact($firstRow, $companyUuid, 'facilitator');
611
+ if ($contact) {
612
+ $facilitatorUuid = $contact->uuid;
613
+ $facilitatorType = 'Fleetbase\\FleetOps\\Models\\Contact';
614
+ }
615
+ } else {
616
+ $vendor = $this->resolveOrCreateVendor($firstRow, $companyUuid, 'facilitator');
617
+ if ($vendor) {
618
+ $facilitatorUuid = $vendor->uuid;
619
+ $facilitatorType = 'Fleetbase\\FleetOps\\Models\\Vendor';
620
+ }
621
+ }
622
+ }
623
+
624
+ // ── Resolve Vehicle ───────────────────────────────────────────
625
+ $vehicleUuid = null;
626
+ if (!empty($firstRow['vehicle_plate'])) {
627
+ $vehicle = Vehicle::where('company_uuid', $companyUuid)
628
+ ->where('plate_number', $firstRow['vehicle_plate'])
629
+ ->first();
630
+ if ($vehicle) {
631
+ $vehicleUuid = $vehicle->uuid;
632
+ }
633
+ }
634
+
635
+ // ── Resolve Driver ────────────────────────────────────────────
636
+ $driverUuid = null;
637
+ $driverIdentifier = $firstRow['driver_email'] ?? $firstRow['driver_phone'] ?? $firstRow['driver_name'] ?? null;
638
+ if ($driverIdentifier) {
639
+ $driver = Driver::findByIdentifier($driverIdentifier);
640
+ if ($driver) {
641
+ $driverUuid = $driver->uuid;
642
+ }
643
+ }
644
+
645
+ // ── Build required_skills array ───────────────────────────────
646
+ $requiredSkills = [];
647
+ if (!empty($firstRow['required_skills'])) {
648
+ $requiredSkills = array_filter(array_map('trim', explode(',', $firstRow['required_skills'])));
649
+ }
650
+
651
+ // ── Create the Order ─────────────────────────────────────────
652
+ $order = Order::create([
653
+ 'company_uuid' => $companyUuid,
654
+ 'order_config_uuid' => $orderConfigUuid,
655
+ 'customer_uuid' => $customerUuid,
656
+ 'customer_type' => $customerType,
657
+ 'facilitator_uuid' => $facilitatorUuid,
658
+ 'facilitator_type' => $facilitatorType,
659
+ 'vehicle_assigned_uuid' => $vehicleUuid,
660
+ 'driver_assigned_uuid' => $driverUuid,
661
+ 'internal_id' => $firstRow['internal_id'] ?? null,
662
+ 'status' => $firstRow['status'] ?? 'created',
663
+ 'type' => $firstRow['type'] ?? 'default',
664
+ 'notes' => $firstRow['notes'] ?? null,
665
+ 'scheduled_at' => !empty($firstRow['scheduled_at'])
666
+ ? Carbon::parse($firstRow['scheduled_at'])
667
+ : null,
668
+ 'time_window_start' => $firstRow['time_window_start'] ?? null,
669
+ 'time_window_end' => $firstRow['time_window_end'] ?? null,
670
+ 'required_skills' => $requiredSkills,
671
+ 'orchestrator_priority' => (int) ($firstRow['priority'] ?? 0),
672
+ 'meta' => array_filter([
673
+ 'service_time_min' => $firstRow['service_time_min'] ?? null,
674
+ 'order_type' => $orderType,
675
+ 'order_ref' => $firstRow['order_ref'] ?? null,
676
+ ]),
677
+ ]);
678
+
679
+ // ── Create Payload ────────────────────────────────────────────
680
+ $payload = Payload::create([
681
+ 'company_uuid' => $companyUuid,
682
+ 'cod_amount' => $firstRow['cod_amount'] ?? null,
683
+ 'cod_currency' => $firstRow['cod_currency'] ?? null,
684
+ 'capacity_weight_kg' => $firstRow['weight_kg'] ?? null,
685
+ 'capacity_volume_m3' => $firstRow['volume_m3'] ?? null,
686
+ 'capacity_parcels' => $firstRow['parcels'] ?? null,
687
+ ]);
688
+
689
+ $order->setPayload($payload);
690
+ $order->save();
691
+
692
+ // ── Attach Places ────────────────────────────────────────────────
693
+ $entities = [];
694
+ if ($isMulti) {
695
+ // ── Multi-waypoint ───────────────────────────────────────
696
+ // Each row with an address becomes a waypoint stop, tagged
697
+ // with a stable _import_id ('wp_0', 'wp_1', …).
698
+ //
699
+ // Multiple entities per order are supported: any row in the
700
+ // group that has entity fields will produce an entity. If
701
+ // the row also has an address it is linked to that waypoint
702
+ // via _import_id; otherwise entity_destination is used to
703
+ // resolve the target stop (index, 'pickup', or 'dropoff').
704
+ $waypoints = [];
705
+ $wpImportIds = []; // rowIndex => _import_id for address rows
706
+
707
+ foreach ($groupRows as $wpIndex => $wpRow) {
708
+ $importId = 'wp_' . $wpIndex;
709
+ $placeData = $this->buildPlaceData($wpRow, 'dropoff');
710
+ if (!empty($placeData['street1'])) {
711
+ $place = Place::createFromMixed($placeData, [], true);
712
+ if ($place) {
713
+ $waypoints[] = [
714
+ 'place_uuid' => $place->uuid,
715
+ 'type' => 'dropoff',
716
+ '_import_id' => $importId,
717
+ ];
718
+ $wpImportIds[$wpIndex] = $importId;
719
+ }
720
+ }
721
+ }
722
+
723
+ if (!empty($waypoints)) {
724
+ $payload->setWaypoints($waypoints);
725
+ }
726
+
727
+ // Collect entities from ALL rows in the group
728
+ foreach ($groupRows as $wpIndex => $wpRow) {
729
+ $entityData = $this->buildEntityData($wpRow, $companyUuid);
730
+ if ($entityData === null) {
731
+ continue;
732
+ }
733
+
734
+ // Prefer the _import_id of this row's own waypoint;
735
+ // fall back to entity_destination if the row has no address.
736
+ if (isset($wpImportIds[$wpIndex])) {
737
+ $entityData['_import_id'] = $wpImportIds[$wpIndex];
738
+ } else {
739
+ $dest = $wpRow['entity_destination'] ?? null;
740
+ if ($dest !== null && $dest !== '') {
741
+ // Numeric string → waypoint index; otherwise 'pickup'/'dropoff'
742
+ if (is_numeric($dest)) {
743
+ $targetIndex = (int) $dest;
744
+ if (isset($wpImportIds[$targetIndex])) {
745
+ $entityData['_import_id'] = $wpImportIds[$targetIndex];
746
+ }
747
+ } else {
748
+ $entityData['destination'] = $dest;
749
+ }
750
+ }
751
+ }
752
+
753
+ $entities[] = $entityData;
754
+ }
755
+ } else {
756
+ // ── Pickup / Dropoff ─────────────────────────────────────
757
+ // Address fields come from the first row only.
758
+ // Entity rows: ALL rows in the group are scanned so that
759
+ // multiple entities (one per row) can be attached to the
760
+ // same order. Only the first row is used for addresses.
761
+ $pickupData = $this->buildPlaceData($firstRow, 'pickup');
762
+ $dropoffData = $this->buildPlaceData($firstRow, 'dropoff');
763
+
764
+ if (!empty($pickupData['street1'])) {
765
+ $payload->setPickup($pickupData, ['save' => true]);
766
+ }
767
+
768
+ if (!empty($dropoffData['street1'])) {
769
+ $payload->setDropoff($dropoffData, ['save' => true]);
770
+ }
771
+
772
+ // Collect entities from ALL rows in the group
773
+ foreach ($groupRows as $entityRow) {
774
+ $entityData = $this->buildEntityData($entityRow, $companyUuid);
775
+ if ($entityData === null) {
776
+ continue;
777
+ }
778
+ // Resolve destination from each row's entity_destination
779
+ // column; default to 'dropoff' when not set.
780
+ $dest = $entityRow['entity_destination'] ?? 'dropoff';
781
+ $entityData['destination'] = ($dest !== '') ? $dest : 'dropoff';
782
+ $entities[] = $entityData;
783
+ }
784
+ }
785
+
786
+ // ── Attach Entities to Payload ──────────────────────────────
787
+ if (!empty($entities)) {
788
+ $payload->load(['waypoints', 'pickup', 'dropoff']);
789
+ $payload->setEntities($entities);
790
+ }
791
+
792
+ DB::commit();
793
+ $created[] = $order->public_id;
794
+ } catch (\Exception $e) {
795
+ DB::rollBack();
796
+ $rowIndex = ($groupRows[0]['_rowIndex'] ?? '?');
797
+ $failed[] = ['row' => $rowIndex, 'error' => $e->getMessage()];
798
+ }
799
+ }
800
+
801
+ return response()->json([
802
+ 'created' => $created,
803
+ 'failed' => $failed,
804
+ ]);
805
+ }
806
+
807
+ // ── Private helpers ───────────────────────────────────────────────────────
808
+
809
+ /**
810
+ * Build a Place-compatible attributes array from a CSV row.
811
+ *
812
+ * @param array $row the mapped CSV row
813
+ * @param string $prefix 'pickup' or 'dropoff'
814
+ */
815
+ private function buildPlaceData(array $row, string $prefix): array
816
+ {
817
+ return array_filter([
818
+ 'name' => $row["{$prefix}_name"] ?? null,
819
+ 'street1' => $row["{$prefix}_street1"] ?? null,
820
+ 'street2' => $row["{$prefix}_street2"] ?? null,
821
+ 'city' => $row["{$prefix}_city"] ?? null,
822
+ 'province' => $row["{$prefix}_state"] ?? null,
823
+ 'postal_code' => $row["{$prefix}_postal_code"] ?? null,
824
+ 'country' => $row["{$prefix}_country"] ?? null,
825
+ 'phone' => $row["{$prefix}_phone"] ?? null,
826
+ 'location' => $this->buildLocationPoint($row["{$prefix}_lat"] ?? null, $row["{$prefix}_lng"] ?? null),
827
+ ]);
828
+ }
829
+
830
+ /**
831
+ * Build a WKT POINT string from optional lat/lng strings.
832
+ * Returns null when either value is missing or both are zero.
833
+ */
834
+ private function buildLocationPoint(?string $lat, ?string $lng): ?string
835
+ {
836
+ if (empty($lat) || empty($lng)) {
837
+ return null;
838
+ }
839
+ $latF = (float) $lat;
840
+ $lngF = (float) $lng;
841
+ if ($latF === 0.0 && $lngF === 0.0) {
842
+ return null;
843
+ }
844
+
845
+ return "POINT({$lngF} {$latF})";
846
+ }
847
+
848
+ /**
849
+ * Build an Entity-compatible attributes array from a CSV row.
850
+ * Returns null when no entity fields are present in the row.
851
+ *
852
+ * @param array $row the mapped CSV row
853
+ * @param string $companyUuid the current company UUID
854
+ */
855
+ private function buildEntityData(array $row, string $companyUuid): ?array
856
+ {
857
+ // Only build an entity if at least one entity field has a value
858
+ $hasEntity = !empty($row['entity_name'])
859
+ || !empty($row['entity_type'])
860
+ || !empty($row['entity_sku'])
861
+ || !empty($row['entity_barcode'])
862
+ || !empty($row['entity_description']);
863
+
864
+ if (!$hasEntity) {
865
+ return null;
866
+ }
867
+
868
+ return array_filter([
869
+ 'company_uuid' => $companyUuid,
870
+ 'name' => $row['entity_name'] ?? null,
871
+ 'type' => $row['entity_type'] ?? null,
872
+ 'description' => $row['entity_description'] ?? null,
873
+ 'sku' => $row['entity_sku'] ?? null,
874
+ 'barcode' => $row['entity_barcode'] ?? null,
875
+ 'internal_id' => $row['entity_internal_id'] ?? null,
876
+ 'declared_value' => isset($row['entity_declared_value']) && $row['entity_declared_value'] !== ''
877
+ ? (float) $row['entity_declared_value'] : null,
878
+ 'currency' => $row['entity_currency'] ?? null,
879
+ 'price' => isset($row['entity_price']) && $row['entity_price'] !== ''
880
+ ? (float) $row['entity_price'] : null,
881
+ 'sale_price' => isset($row['entity_sale_price']) && $row['entity_sale_price'] !== ''
882
+ ? (float) $row['entity_sale_price'] : null,
883
+ 'weight' => isset($row['entity_weight']) && $row['entity_weight'] !== ''
884
+ ? (float) $row['entity_weight'] : null,
885
+ 'weight_unit' => $row['entity_weight_unit'] ?? null,
886
+ 'length' => isset($row['entity_length']) && $row['entity_length'] !== ''
887
+ ? (float) $row['entity_length'] : null,
888
+ 'width' => isset($row['entity_width']) && $row['entity_width'] !== ''
889
+ ? (float) $row['entity_width'] : null,
890
+ 'height' => isset($row['entity_height']) && $row['entity_height'] !== ''
891
+ ? (float) $row['entity_height'] : null,
892
+ 'dimensions_unit' => $row['entity_dimensions_unit'] ?? null,
893
+ ], fn ($v) => $v !== null);
894
+ }
895
+
896
+ /**
897
+ * Resolve an existing Contact by email/phone, or create a new one.
898
+ *
899
+ * @param array $row the mapped CSV row
900
+ * @param string $companyUuid the company UUID
901
+ * @param string $prefix 'customer' or 'facilitator'
902
+ */
903
+ private function resolveOrCreateContact(array $row, string $companyUuid, string $prefix): ?Contact
904
+ {
905
+ $email = $row["{$prefix}_email"] ?? null;
906
+ $phone = $row["{$prefix}_phone"] ?? null;
907
+ $name = $row["{$prefix}_name"] ?? null;
908
+
909
+ // Try to find by email first, then phone.
910
+ $contact = null;
911
+ if ($email) {
912
+ $contact = Contact::where('company_uuid', $companyUuid)->where('email', $email)->first();
913
+ }
914
+ if (!$contact && $phone) {
915
+ $contact = Contact::where('company_uuid', $companyUuid)->where('phone', $phone)->first();
916
+ }
917
+
918
+ if ($contact) {
919
+ return $contact;
920
+ }
921
+
922
+ // Create a new contact if we have at least a name or email.
923
+ if ($name || $email) {
924
+ return Contact::create(array_filter([
925
+ 'company_uuid' => $companyUuid,
926
+ 'name' => $name,
927
+ 'email' => $email,
928
+ 'phone' => $phone,
929
+ 'type' => 'customer',
930
+ ]));
931
+ }
932
+
933
+ return null;
934
+ }
935
+
936
+ /**
937
+ * Resolve an existing Vendor by email/phone, or create a new one.
938
+ *
939
+ * @param array $row the mapped CSV row
940
+ * @param string $companyUuid the company UUID
941
+ * @param string $prefix 'customer' or 'facilitator'
942
+ */
943
+ private function resolveOrCreateVendor(array $row, string $companyUuid, string $prefix): ?Vendor
944
+ {
945
+ $email = $row["{$prefix}_email"] ?? null;
946
+ $phone = $row["{$prefix}_phone"] ?? null;
947
+ $name = $row["{$prefix}_name"] ?? null;
948
+
949
+ $vendor = null;
950
+ if ($email) {
951
+ $vendor = Vendor::where('company_uuid', $companyUuid)->where('email', $email)->first();
952
+ }
953
+ if (!$vendor && $phone) {
954
+ $vendor = Vendor::where('company_uuid', $companyUuid)->where('phone', $phone)->first();
955
+ }
956
+ if (!$vendor && $name) {
957
+ $vendor = Vendor::where('company_uuid', $companyUuid)->whereRaw('lower(name) = ?', [strtolower($name)])->first();
958
+ }
959
+
960
+ if ($vendor) {
961
+ return $vendor;
962
+ }
963
+
964
+ if ($name || $email) {
965
+ return Vendor::create(array_filter([
966
+ 'company_uuid' => $companyUuid,
967
+ 'name' => $name,
968
+ 'email' => $email,
969
+ 'phone' => $phone,
970
+ ]));
971
+ }
972
+
973
+ return null;
974
+ }
975
+ }