@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
@@ -3,12 +3,7 @@
3
3
  <div class="mb-3 flex items-center justify-between">
4
4
  <div class="flex items-center">
5
5
  <div class="fleet-vehicle-listing-search">
6
- <Input
7
- @type="text"
8
- {{on "input" this.onInput}}
9
- placeholder={{t "fleet-panel.vehicle-listing.search-vehicle"}}
10
- class="w-64 form-input form-input-sm"
11
- />
6
+ <Input @type="text" {{on "input" this.onInput}} placeholder={{t "fleet-panel.vehicle-listing.search-vehicle"}} class="w-64 form-input form-input-sm" />
12
7
  </div>
13
8
  {{#if this.search.isRunning}}
14
9
  <div class="mx-2">
@@ -77,11 +77,7 @@
77
77
  @disabled={{cannot-write @resource}}
78
78
  />
79
79
  </InputGroup>
80
- <InputGroup
81
- @name={{t "fuel-report.fields.fuel-volume"}}
82
- @helpText={{t "fuel-report.fields.select-volume-help-text"}}
83
- @wrapperClass="w-full"
84
- >
80
+ <InputGroup @name={{t "fuel-report.fields.fuel-volume"}} @helpText={{t "fuel-report.fields.select-volume-help-text"}} @wrapperClass="w-full">
85
81
  <UnitInput
86
82
  class="w-full"
87
83
  @measurement="volume"
@@ -135,44 +135,48 @@
135
135
  >{{if menuItem.intl (t menuItem.intl) menuItem.title}}</Layout::Sidebar::Item>
136
136
  {{/if}}
137
137
  {{/each}}
138
- {{#if (eq menuPanel.routePrefix "settings")}}
139
- {{#each this.universeSettingsMenuItems as |menuItem|}}
140
- {{#if menuItem.renderComponentInPlace}}
141
- {{component menuItem.component}}
142
- {{else}}
143
- <Layout::Sidebar::Item
144
- @onClick={{fn this.universe.transitionMenuItem (concat this.routePrefix "virtual") menuItem}}
145
- @menuItem={{menuItem}}
146
- @icon={{menuItem.icon}}
147
- @iconComponent={{menuItem.iconComponent}}
148
- @iconComponentOptions={{menuItem.iconComponentOptions}}
149
- @iconSize={{menuItem.iconSize}}
150
- @iconPrefix={{menuItem.iconPrefix}}
151
- @iconClass={{menuItem.iconClass}}
152
- @rightSideComponent={{menuItem.rightSideComponent}}
153
- @rightSideComponentContext={{menuItem.rightSideComponentContext}}
154
- @rightSideStatus={{menuItem.rightSideStatus}}
155
- @rightSideStatusText={{menuItem.rightSideStatusText}}
156
- @hideRightSideStatusDot={{menuItem.hideRightSideStatusDot}}
157
- @rightSideStatusClass={{menuItem.rightSideStatusClass}}
158
- @disableRightSideStatusHumanize={{menuItem.disableRightSideStatusHumanize}}
159
- @dropdownButton={{menuItem.dropdownButton}}
160
- @dropdownButtonActions={{menuItem.dropdownButtonActions}}
161
- @dropdownButtonIcon={{menuItem.dropdownButtonIcon}}
162
- @dropdownButtonIconPrefix={{menuItem.dropdownButtonIconPrefix}}
163
- @dropdownButtonText={{menuItem.dropdownButtonText}}
164
- @dropdownButtonCalculatePosition={{menuItem.dropdownButtonCalculatePosition}}
165
- @dropdownButtonRenderInPlace={{menuItem.dropdownButtonRenderInPlace}}
166
- @dropdownButtonMenuLabel={{menuItem.dropdownButtonMenuLabel}}
167
- @dropdownButtonWrapperClass={{menuItem.dropdownButtonWrapperClass}}
168
- @dropdownButtonTriggerClass={{menuItem.dropdownButtonTriggerClass}}
169
- @registerDropdownButtonAPI={{menuItem.registerDropdownButtonAPI}}
170
- @permission={{menuItem.permission}}
171
- @visible={{menuItem.visible}}
172
- >{{if menuItem.intl (t menuItem.intl) menuItem.title}}</Layout::Sidebar::Item>
138
+ {{#let (array "operations" "management" "connectivity" "maintenance" "analytics" "settings") as |sectionsToRender|}}
139
+ {{#each sectionsToRender as |section|}}
140
+ {{#if (eq menuPanel.routePrefix section)}}
141
+ {{#each (get this (concat "universe" (capitalize section) "MenuItems")) as |menuItem|}}
142
+ {{#if menuItem.renderComponentInPlace}}
143
+ {{component menuItem.component}}
144
+ {{else}}
145
+ <Layout::Sidebar::Item
146
+ @onClick={{fn this.universe.transitionMenuItem (concat this.routePrefix "virtual") menuItem}}
147
+ @menuItem={{menuItem}}
148
+ @icon={{menuItem.icon}}
149
+ @iconComponent={{menuItem.iconComponent}}
150
+ @iconComponentOptions={{menuItem.iconComponentOptions}}
151
+ @iconSize={{menuItem.iconSize}}
152
+ @iconPrefix={{menuItem.iconPrefix}}
153
+ @iconClass={{menuItem.iconClass}}
154
+ @rightSideComponent={{menuItem.rightSideComponent}}
155
+ @rightSideComponentContext={{menuItem.rightSideComponentContext}}
156
+ @rightSideStatus={{menuItem.rightSideStatus}}
157
+ @rightSideStatusText={{menuItem.rightSideStatusText}}
158
+ @hideRightSideStatusDot={{menuItem.hideRightSideStatusDot}}
159
+ @rightSideStatusClass={{menuItem.rightSideStatusClass}}
160
+ @disableRightSideStatusHumanize={{menuItem.disableRightSideStatusHumanize}}
161
+ @dropdownButton={{menuItem.dropdownButton}}
162
+ @dropdownButtonActions={{menuItem.dropdownButtonActions}}
163
+ @dropdownButtonIcon={{menuItem.dropdownButtonIcon}}
164
+ @dropdownButtonIconPrefix={{menuItem.dropdownButtonIconPrefix}}
165
+ @dropdownButtonText={{menuItem.dropdownButtonText}}
166
+ @dropdownButtonCalculatePosition={{menuItem.dropdownButtonCalculatePosition}}
167
+ @dropdownButtonRenderInPlace={{menuItem.dropdownButtonRenderInPlace}}
168
+ @dropdownButtonMenuLabel={{menuItem.dropdownButtonMenuLabel}}
169
+ @dropdownButtonWrapperClass={{menuItem.dropdownButtonWrapperClass}}
170
+ @dropdownButtonTriggerClass={{menuItem.dropdownButtonTriggerClass}}
171
+ @registerDropdownButtonAPI={{menuItem.registerDropdownButtonAPI}}
172
+ @permission={{menuItem.permission}}
173
+ @visible={{menuItem.visible}}
174
+ >{{if menuItem.intl (t menuItem.intl) menuItem.title}}</Layout::Sidebar::Item>
175
+ {{/if}}
176
+ {{/each}}
173
177
  {{/if}}
174
178
  {{/each}}
175
- {{/if}}
179
+ {{/let}}
176
180
  </Layout::Sidebar::Panel>
177
181
  {{/each}}
178
182
  {{#each this.universeMenuPanels as |menuPanel|}}
@@ -15,6 +15,11 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
15
15
  @tracked routePrefix = 'console.fleet-ops.';
16
16
  @tracked menuPanels = [];
17
17
  @tracked universeMenuItems = [];
18
+ @tracked universeOperationsMenuItems = [];
19
+ @tracked universeManagementMenuItems = [];
20
+ @tracked universeConnectivityMenuItems = [];
21
+ @tracked universeMaintenanceMenuItems = [];
22
+ @tracked universeAnalyticsMenuItems = [];
18
23
  @tracked universeSettingsMenuItems = [];
19
24
  @tracked universeMenuPanels = [];
20
25
 
@@ -26,8 +31,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
26
31
 
27
32
  createMenuItemsFromUniverseRegistry() {
28
33
  const registeredMenuItems = this.menuService.getMenuItems('engine:fleet-ops');
34
+ console.log('Registered menu items for engine:fleet-ops:', registeredMenuItems);
29
35
  this.universeMenuPanels = this.menuService.getMenuPanels('engine:fleet-ops');
30
36
  this.universeMenuItems = registeredMenuItems.filter((menuItem) => menuItem.section === undefined);
37
+ this.universeOperationsMenuItems = registeredMenuItems.filter((menuItem) => menuItem.section === 'operations');
38
+ this.universeManagementMenuItems = registeredMenuItems.filter((menuItem) => menuItem.section === 'management');
39
+ this.universeMaintenanceMenuItems = registeredMenuItems.filter((menuItem) => menuItem.section === 'maintenance');
40
+ this.universeConnectivityMenuItems = registeredMenuItems.filter((menuItem) => menuItem.section === 'connectivity');
41
+ this.universeAnalyticsMenuItems = registeredMenuItems.filter((menuItem) => menuItem.section === 'analytics');
31
42
  this.universeSettingsMenuItems = registeredMenuItems.filter((menuItem) => menuItem.section === 'settings');
32
43
  }
33
44
 
@@ -43,12 +54,12 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
43
54
  visible: this.abilities.can('fleet-ops see order'),
44
55
  },
45
56
  {
46
- intl: 'menu.service-rates',
47
- title: this.intl.t('menu.service-rates'),
48
- icon: 'file-invoice-dollar',
49
- route: 'operations.service-rates',
50
- permission: 'fleet-ops list service-rate',
51
- visible: this.abilities.can('fleet-ops see service-rate'),
57
+ intl: 'menu.orchestrator',
58
+ title: this.intl.t('menu.orchestrator'),
59
+ icon: 'circle-nodes',
60
+ route: 'operations.orchestrator',
61
+ permission: 'fleet-ops list order',
62
+ visible: this.abilities.can('fleet-ops see order'),
52
63
  },
53
64
  {
54
65
  intl: 'menu.scheduler',
@@ -66,6 +77,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
66
77
  permission: 'fleet-ops list order-config',
67
78
  visible: this.abilities.can('fleet-ops see order-config'),
68
79
  },
80
+ {
81
+ intl: 'menu.service-rates',
82
+ title: this.intl.t('menu.service-rates'),
83
+ icon: 'file-invoice-dollar',
84
+ route: 'operations.service-rates',
85
+ permission: 'fleet-ops list service-rate',
86
+ visible: this.abilities.can('fleet-ops see service-rate'),
87
+ },
69
88
  ];
70
89
 
71
90
  const resourcesItems = [
@@ -183,6 +202,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
183
202
  ];
184
203
 
185
204
  const maintenanceItems = [
205
+ {
206
+ intl: 'menu.schedules',
207
+ title: this.intl.t('menu.schedules'),
208
+ icon: 'calendar-alt',
209
+ route: 'maintenance.schedules',
210
+ permission: 'fleet-ops list maintenance-schedule',
211
+ visible: this.abilities.can('fleet-ops see maintenance-schedule'),
212
+ },
186
213
  {
187
214
  intl: 'menu.work-orders',
188
215
  title: this.intl.t('menu.work-orders'),
@@ -191,6 +218,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
191
218
  permission: 'fleet-ops list work-order',
192
219
  visible: this.abilities.can('fleet-ops see work-order'),
193
220
  },
221
+ {
222
+ intl: 'menu.maintenances',
223
+ title: this.intl.t('menu.maintenances'),
224
+ icon: 'history',
225
+ route: 'maintenance.maintenances',
226
+ permission: 'fleet-ops list maintenance',
227
+ visible: this.abilities.can('fleet-ops see maintenance'),
228
+ },
194
229
  {
195
230
  intl: 'menu.equipment',
196
231
  title: this.intl.t('menu.equipment'),
@@ -253,6 +288,22 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
253
288
  permission: 'fleet-ops view routing-settings',
254
289
  visible: this.abilities.can('fleet-ops see routing-settings'),
255
290
  },
291
+ {
292
+ intl: 'menu.orchestrator',
293
+ title: this.intl.t('menu.orchestrator'),
294
+ icon: 'circle-nodes',
295
+ route: 'settings.orchestrator',
296
+ permission: 'fleet-ops view routing-settings',
297
+ visible: this.abilities.can('fleet-ops see routing-settings'),
298
+ },
299
+ {
300
+ intl: 'menu.scheduling',
301
+ title: this.intl.t('menu.scheduling'),
302
+ icon: 'calendar-days',
303
+ route: 'settings.scheduling',
304
+ permission: 'fleet-ops view scheduling-settings',
305
+ visible: this.abilities.can('fleet-ops see scheduling-settings'),
306
+ },
256
307
  {
257
308
  intl: 'menu.custom-fields',
258
309
  title: this.intl.t('menu.custom-fields'),
@@ -289,10 +340,10 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
289
340
  open: this.appCache.get('fleet-ops:sidebar:management:open', true),
290
341
  onToggle: (open) => this.appCache.set('fleet-ops:sidebar:management:open', open),
291
342
  }),
292
- // createPanel('menu.maintenance', 'maintenance', maintenanceItems, {
293
- // open: this.appCache.get('fleet-ops:sidebar:maintenance:open', false),
294
- // onToggle: (open) => this.appCache.set('fleet-ops:sidebar:maintenance:open', open),
295
- // }),
343
+ createPanel('menu.maintenance', 'maintenance', maintenanceItems, {
344
+ open: this.appCache.get('fleet-ops:sidebar:maintenance:open', false),
345
+ onToggle: (open) => this.appCache.set('fleet-ops:sidebar:maintenance:open', open),
346
+ }),
296
347
  createPanel('menu.connectivity', 'connectivity', connectivityItems, {
297
348
  open: this.appCache.get('fleet-ops:sidebar:connectivity:open', false),
298
349
  onToggle: (open) => this.appCache.set('fleet-ops:sidebar:connectivity:open', open),
@@ -0,0 +1,176 @@
1
+ {{! ===== COST PANEL — invoice-style line items ===== }}
2
+ <ContentPanel @title="Costs" @open={{true}} @wrapperClass="bordered-top">
3
+
4
+ {{! ── Currency selector (single, shared by all money fields) ── }}
5
+ <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs mb-2">
6
+ <InputGroup @name="Currency">
7
+ <CurrencySelect @currency={{@resource.currency}} @onCurrencyChange={{fn (mut @resource.currency)}} @triggerClass="w-full form-select" @disabled={{this.isDisabled}} />
8
+ </InputGroup>
9
+ </div>
10
+
11
+ {{! ── Labour & Tax inputs (always editable on the form) ── }}
12
+ <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs mb-4">
13
+ <InputGroup @name="Labour Cost">
14
+ <MoneyInput class="w-full" @value={{@resource.labor_cost}} @currency={{this.currency}} @onChange={{fn (mut @resource.labor_cost)}} @disabled={{this.isDisabled}} />
15
+ </InputGroup>
16
+ <InputGroup @name="Tax">
17
+ <MoneyInput class="w-full" @value={{@resource.tax}} @currency={{this.currency}} @onChange={{fn (mut @resource.tax)}} @disabled={{this.isDisabled}} />
18
+ </InputGroup>
19
+ </div>
20
+
21
+ {{! ── Line items table ── }}
22
+ <div class="text-[11px] uppercase tracking-wide text-gray-500 dark:text-gray-400 font-semibold mb-2">
23
+ Parts &amp; Services
24
+ </div>
25
+
26
+ <table class="w-full text-xs border-collapse mb-2 table-fixed">
27
+ <colgroup>
28
+ <col class="w-[45%]" />
29
+ <col class="w-[10%]" />
30
+ <col class="w-[20%]" />
31
+ <col class="w-[15%]" />
32
+ {{#unless this.isDisabled}}
33
+ <col class="w-[10%]" />
34
+ {{/unless}}
35
+ </colgroup>
36
+ <thead>
37
+ <tr class="border-b border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 text-left">
38
+ <th class="pb-1 pr-2 font-medium">Description</th>
39
+ <th class="pb-1 pr-2 font-medium text-right">Qty</th>
40
+ <th class="pb-1 pr-2 font-medium text-right">Unit Cost</th>
41
+ <th class="pb-1 pr-2 font-medium text-right">Total</th>
42
+ {{#unless this.isDisabled}}
43
+ <th class="pb-1"></th>
44
+ {{/unless}}
45
+ </tr>
46
+ </thead>
47
+ <tbody>
48
+ {{#each this.lineItems as |item index|}}
49
+ {{#if (eq this.editingIndex index)}}
50
+ {{! ── Inline edit row ── }}
51
+ <tr class="border-b border-gray-100 dark:border-gray-800">
52
+ <td class="py-1 pr-2">
53
+ <Input @value={{this.draftDescription}} class="form-input w-full text-xs" placeholder="Description" />
54
+ </td>
55
+ <td class="py-1 pr-2">
56
+ <Input @value={{this.draftQuantity}} @type="number" class="form-input w-full text-xs text-right" min="0" />
57
+ </td>
58
+ <td class="py-1 pr-2">
59
+ <MoneyInput class="w-full" @value={{this.draftUnitCost}} @currency={{this.currency}} @onChange={{this.setDraftUnitCost}} />
60
+ </td>
61
+ <td class="py-1 pr-2 text-right text-gray-500 dark:text-gray-400">
62
+ {{format-currency this.draftLineTotal this.currency}}
63
+ </td>
64
+ <td class="py-1">
65
+ <div class="flex items-center space-x-1">
66
+ <Button @size="xs" @type="primary" @onClick={{this.saveEdit}} class="px-2 py-0.5">
67
+ Save
68
+ </Button>
69
+ <Button @size="xs" @type="default" @onClick={{this.cancelEdit}} class="px-2 py-0.5">
70
+ Cancel
71
+ </Button>
72
+ </div>
73
+ </td>
74
+ </tr>
75
+ {{else}}
76
+ {{! ── Read row ── }}
77
+ <tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/40 group">
78
+ <td class="py-1.5 pr-2 text-gray-800 dark:text-gray-200 truncate">{{item.description}}</td>
79
+ <td class="py-1.5 pr-2 text-right text-gray-600 dark:text-gray-400">{{item.quantity}}</td>
80
+ <td class="py-1.5 pr-2 text-right text-gray-600 dark:text-gray-400">
81
+ {{format-currency (this._toCents item.unit_cost) this.currency}}
82
+ </td>
83
+ <td class="py-1.5 pr-2 text-right font-medium text-gray-800 dark:text-gray-200">
84
+ {{format-currency (this.lineTotal item) this.currency}}
85
+ </td>
86
+ {{#unless this.isDisabled}}
87
+ <td class="py-1.5">
88
+ <div class="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
89
+ <button
90
+ type="button"
91
+ class="text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
92
+ title="Edit"
93
+ {{on "click" (fn this.startEdit index)}}
94
+ >
95
+ <FaIcon @icon="pencil" @size="xs" />
96
+ </button>
97
+ <button
98
+ type="button"
99
+ class="text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors"
100
+ title="Remove"
101
+ {{on "click" (fn this.removeLineItem index)}}
102
+ >
103
+ <FaIcon @icon="trash" @size="xs" />
104
+ </button>
105
+ </div>
106
+ </td>
107
+ {{/unless}}
108
+ </tr>
109
+ {{/if}}
110
+ {{/each}}
111
+
112
+ {{! ── Add new item row ── }}
113
+ {{#if this.isAddingItem}}
114
+ <tr class="border-b border-gray-100 dark:border-gray-800">
115
+ <td class="py-1 pr-2">
116
+ <Input @value={{this.draftDescription}} class="form-input w-full text-xs" placeholder="Description" />
117
+ </td>
118
+ <td class="py-1 pr-2">
119
+ <Input @value={{this.draftQuantity}} @type="number" class="form-input w-full text-xs text-right" min="0" />
120
+ </td>
121
+ <td class="py-1 pr-2">
122
+ <MoneyInput class="w-full" @value={{this.draftUnitCost}} @currency={{this.currency}} @onChange={{this.setDraftUnitCost}} />
123
+ </td>
124
+ <td class="py-1 pr-2 text-right text-gray-500 dark:text-gray-400">
125
+ {{format-currency this.draftLineTotal this.currency}}
126
+ </td>
127
+ <td class="py-1">
128
+ <div class="flex items-center space-x-1">
129
+ <Button @size="xs" @type="primary" @onClick={{this.addLineItem}} class="px-2 py-0.5">
130
+ Add
131
+ </Button>
132
+ <Button @size="xs" @type="default" @onClick={{this.cancelAdd}} class="px-2 py-0.5">
133
+ Cancel
134
+ </Button>
135
+ </div>
136
+ </td>
137
+ </tr>
138
+ {{/if}}
139
+ </tbody>
140
+ </table>
141
+
142
+ {{! ── Add item button ── }}
143
+ {{#unless this.isDisabled}}
144
+ {{#unless this.isAddingItem}}
145
+ <button
146
+ type="button"
147
+ class="flex items-center space-x-1 text-xs text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 font-medium mb-3 transition-colors"
148
+ {{on "click" this.showAddRow}}
149
+ >
150
+ <FaIcon @icon="plus" @size="xs" />
151
+ <span>Add line item</span>
152
+ </button>
153
+ {{/unless}}
154
+ {{/unless}}
155
+
156
+ {{! ── Totals summary ── }}
157
+ <div class="border-t border-gray-200 dark:border-gray-700 pt-2 mt-1 space-y-1 text-xs">
158
+ <div class="flex justify-between text-gray-500 dark:text-gray-400">
159
+ <span>Labour</span>
160
+ <span>{{format-currency this.laborCost this.currency}}</span>
161
+ </div>
162
+ <div class="flex justify-between text-gray-500 dark:text-gray-400">
163
+ <span>Parts</span>
164
+ <span>{{format-currency this.partsCost this.currency}}</span>
165
+ </div>
166
+ <div class="flex justify-between text-gray-500 dark:text-gray-400">
167
+ <span>Tax</span>
168
+ <span>{{format-currency this.tax this.currency}}</span>
169
+ </div>
170
+ <div class="flex justify-between font-semibold text-gray-800 dark:text-gray-100 border-t border-gray-200 dark:border-gray-700 pt-1 mt-1">
171
+ <span>Total</span>
172
+ <span>{{format-currency this.totalCost this.currency}}</span>
173
+ </div>
174
+ </div>
175
+
176
+ </ContentPanel>
@@ -0,0 +1,241 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { inject as service } from '@ember/service';
5
+ import { isArray } from '@ember/array';
6
+ import numbersOnly from '@fleetbase/ember-core/utils/numbers-only';
7
+
8
+ /**
9
+ * `Maintenance::CostPanel`
10
+ *
11
+ * Renders an invoice-style line items editor for a maintenance record.
12
+ *
13
+ * Line items are stored in the `line_items` JSON column on the maintenance
14
+ * record (`@attr('raw')`). Mutations are purely in-memory — the array is
15
+ * written back onto `@resource.line_items` and the parent form's normal
16
+ * save flow persists everything together in a single request. This means
17
+ * the component works correctly for both new (unsaved) and existing records.
18
+ *
19
+ * All monetary values (unit_cost, labor_cost, tax, parts_cost, total_cost)
20
+ * are stored as **cents** (integers) on the backend via the Money::class cast
21
+ * and surfaced as strings by `@attr('string')` on the Ember model. The
22
+ * `numbersOnly()` utility strips any non-digit characters and `parseInt`
23
+ * converts to a safe integer before arithmetic.
24
+ *
25
+ * The `MoneyInput` component handles the cents ↔ display conversion:
26
+ * - `@value` receives cents (e.g. 1000 = $10.00)
27
+ * - `@onChange` emits cents (e.g. 1000 = $10.00)
28
+ *
29
+ * Usage:
30
+ * <Maintenance::CostPanel @resource={{this.maintenance}} @disabled={{cannot-write this.maintenance}} />
31
+ */
32
+ export default class MaintenanceCostPanelComponent extends Component {
33
+ @service notifications;
34
+ @service intl;
35
+
36
+ /** Whether the "Add line item" row is currently visible */
37
+ @tracked isAddingItem = false;
38
+
39
+ /** Tracks which line item index is currently being edited (null = none) */
40
+ @tracked editingIndex = null;
41
+
42
+ /**
43
+ * Draft values for the add/edit form row.
44
+ * `draftUnitCost` is stored in **cents** (integer) to match the backend
45
+ * Money::class cast and the MoneyInput component's expected input/output format.
46
+ */
47
+ @tracked draftDescription = '';
48
+ @tracked draftQuantity = 1;
49
+ @tracked draftUnitCost = 0;
50
+
51
+ // ─── Helpers ─────────────────────────────────────────────────────────────
52
+
53
+ /**
54
+ * Safely parse a monetary string attribute (cents stored as string) to int.
55
+ */
56
+ @action _toCents(value) {
57
+ if (value === null || value === undefined || value === '') return 0;
58
+ const parsed = parseInt(numbersOnly(String(value)), 10);
59
+ return isNaN(parsed) ? 0 : parsed;
60
+ }
61
+
62
+ // ─── Computed getters ─────────────────────────────────────────────────────
63
+
64
+ get lineItems() {
65
+ return isArray(this.args.resource?.line_items) ? this.args.resource.line_items : [];
66
+ }
67
+
68
+ get currency() {
69
+ return this.args.resource?.currency ?? 'USD';
70
+ }
71
+
72
+ get laborCost() {
73
+ return this._toCents(this.args.resource?.labor_cost);
74
+ }
75
+
76
+ get tax() {
77
+ return this._toCents(this.args.resource?.tax);
78
+ }
79
+
80
+ get partsCost() {
81
+ return this.lineItems.reduce((sum, item) => {
82
+ const qty = parseInt(item.quantity, 10) || 0;
83
+ const cost = this._toCents(item.unit_cost);
84
+ return sum + qty * cost;
85
+ }, 0);
86
+ }
87
+
88
+ get totalCost() {
89
+ return this.laborCost + this.partsCost + this.tax;
90
+ }
91
+
92
+ /**
93
+ * Live preview of the draft line total (in cents).
94
+ * Displayed via `format-currency` which also expects cents.
95
+ */
96
+ get draftLineTotal() {
97
+ return (parseInt(this.draftQuantity, 10) || 0) * this._toCents(this.draftUnitCost);
98
+ }
99
+
100
+ get isDisabled() {
101
+ return this.args.disabled ?? false;
102
+ }
103
+
104
+ // ─── Draft unit cost setter ───────────────────────────────────────────────
105
+
106
+ /**
107
+ * Called by MoneyInput's `@onChange`. The value is already in cents.
108
+ */
109
+ @action
110
+ setDraftUnitCost(cents) {
111
+ this.draftUnitCost = cents ?? 0;
112
+ }
113
+
114
+ // ─── Line item helpers ────────────────────────────────────────────────────
115
+
116
+ /**
117
+ * Returns the line total for a single item (quantity × unit_cost).
118
+ * Both values are in cents, so the result is also in cents.
119
+ * Used in the template as (this.lineTotal item).
120
+ */
121
+ @action
122
+ lineTotal(item) {
123
+ const qty = parseInt(item?.quantity, 10) || 0;
124
+ const cost = this._toCents(item?.unit_cost);
125
+ return qty * cost;
126
+ }
127
+
128
+ /**
129
+ * Write a new copy of the line items array back onto the resource so
130
+ * Ember Data tracks the change and includes it in the next save().
131
+ */
132
+ _commitItems(items) {
133
+ this.args.resource.set('line_items', [...items]);
134
+ // Recompute parts_cost and total_cost locally so the summary updates
135
+ // immediately without waiting for a server round-trip.
136
+ const partsCost = items.reduce((sum, item) => {
137
+ return sum + (parseInt(item.quantity, 10) || 0) * this._toCents(item.unit_cost);
138
+ }, 0);
139
+ this.args.resource.set('parts_cost', String(partsCost));
140
+ this.args.resource.set('total_cost', String(this.laborCost + partsCost + this.tax));
141
+ }
142
+
143
+ // ─── Add item ────────────────────────────────────────────────────────────
144
+
145
+ @action
146
+ showAddRow() {
147
+ this.isAddingItem = true;
148
+ this.editingIndex = null;
149
+ this._resetDraft();
150
+ }
151
+
152
+ @action
153
+ cancelAdd() {
154
+ this.isAddingItem = false;
155
+ this._resetDraft();
156
+ }
157
+
158
+ @action
159
+ addLineItem() {
160
+ if (!this.draftDescription?.trim()) {
161
+ this.notifications.warning('Please enter a description for the line item.');
162
+ return;
163
+ }
164
+
165
+ const newItem = {
166
+ description: this.draftDescription.trim(),
167
+ quantity: parseInt(this.draftQuantity, 10) || 1,
168
+ unit_cost: this.draftUnitCost,
169
+ currency: this.currency,
170
+ };
171
+
172
+ this._commitItems([...this.lineItems, newItem]);
173
+ this.isAddingItem = false;
174
+ this._resetDraft();
175
+ }
176
+
177
+ // ─── Edit item ───────────────────────────────────────────────────────────
178
+
179
+ @action
180
+ startEdit(index) {
181
+ const item = this.lineItems[index];
182
+ if (!item) return;
183
+ this.editingIndex = index;
184
+ this.isAddingItem = false;
185
+ this.draftDescription = item.description ?? '';
186
+ this.draftQuantity = item.quantity ?? 1;
187
+ // unit_cost from the record is already in cents
188
+ this.draftUnitCost = this._toCents(item.unit_cost);
189
+ }
190
+
191
+ @action
192
+ cancelEdit() {
193
+ this.editingIndex = null;
194
+ this._resetDraft();
195
+ }
196
+
197
+ @action
198
+ saveEdit() {
199
+ if (!this.draftDescription?.trim()) {
200
+ this.notifications.warning('Please enter a description for the line item.');
201
+ return;
202
+ }
203
+
204
+ const index = this.editingIndex;
205
+ const updatedItems = this.lineItems.map((item, i) => {
206
+ if (i !== index) return item;
207
+ return {
208
+ ...item,
209
+ description: this.draftDescription.trim(),
210
+ quantity: parseInt(this.draftQuantity, 10) || 1,
211
+ unit_cost: this.draftUnitCost,
212
+ currency: this.currency,
213
+ };
214
+ });
215
+
216
+ this._commitItems(updatedItems);
217
+ this.editingIndex = null;
218
+ this._resetDraft();
219
+ }
220
+
221
+ // ─── Remove item ─────────────────────────────────────────────────────────
222
+
223
+ @action
224
+ removeLineItem(index) {
225
+ const updatedItems = this.lineItems.filter((_, i) => i !== index);
226
+ this._commitItems(updatedItems);
227
+ // If we were editing this item, close the edit row
228
+ if (this.editingIndex === index) {
229
+ this.editingIndex = null;
230
+ this._resetDraft();
231
+ }
232
+ }
233
+
234
+ // ─── Private helpers ─────────────────────────────────────────────────────
235
+
236
+ _resetDraft() {
237
+ this.draftDescription = '';
238
+ this.draftQuantity = 1;
239
+ this.draftUnitCost = 0;
240
+ }
241
+ }