@fleetbase/fleetops-engine 0.6.65 → 0.6.66

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 (1051) hide show
  1. package/RELEASE.md +11 -6
  2. package/addon/components/ai/create-order-preview.hbs +10 -5
  3. package/addon/components/asset/pill.hbs +24 -0
  4. package/addon/components/asset/summary.hbs +1 -0
  5. package/addon/components/asset-connection/pill.hbs +24 -0
  6. package/addon/components/asset-connection/summary.hbs +1 -0
  7. package/addon/components/attachable/pill.hbs +24 -0
  8. package/addon/components/attachable/summary.hbs +1 -0
  9. package/addon/components/cell/asset-connection-identity.hbs +1 -0
  10. package/addon/components/cell/asset-identity.hbs +1 -0
  11. package/addon/components/cell/attachable-identity.hbs +1 -0
  12. package/addon/components/cell/contact-identity.hbs +1 -0
  13. package/addon/components/cell/customer-identity.hbs +1 -0
  14. package/addon/components/cell/device-event-identity.hbs +1 -0
  15. package/addon/components/cell/device-identity.hbs +1 -24
  16. package/addon/components/cell/driver-identity.hbs +1 -33
  17. package/addon/components/cell/entity-identity.hbs +1 -0
  18. package/addon/components/cell/equipment-identity.hbs +1 -5
  19. package/addon/components/cell/facilitator-identity.hbs +1 -0
  20. package/addon/components/cell/fleet-driver-identity.hbs +1 -0
  21. package/addon/components/cell/fleet-identity.hbs +1 -0
  22. package/addon/components/cell/fuel-provider-connection-identity.hbs +1 -0
  23. package/addon/components/cell/fuel-provider-transaction-identity.hbs +1 -0
  24. package/addon/components/cell/fuel-report-identity.hbs +1 -0
  25. package/addon/components/cell/inspection-form-identity.hbs +1 -0
  26. package/addon/components/cell/inspection-item-result-identity.hbs +1 -0
  27. package/addon/components/cell/inspection-submission-identity.hbs +1 -0
  28. package/addon/components/cell/integrated-vendor-identity.hbs +1 -0
  29. package/addon/components/cell/issue-identity.hbs +1 -0
  30. package/addon/components/cell/maintenance-identity.hbs +1 -0
  31. package/addon/components/cell/maintenance-schedule-identity.hbs +1 -0
  32. package/addon/components/cell/maintenance-subject-identity.hbs +1 -0
  33. package/addon/components/cell/manifest-identity.hbs +1 -0
  34. package/addon/components/cell/manifest-stop-identity.hbs +1 -0
  35. package/addon/components/cell/order-config-identity.hbs +1 -0
  36. package/addon/components/cell/order-identity.hbs +1 -0
  37. package/addon/components/cell/part-identity.hbs +1 -5
  38. package/addon/components/cell/payload-identity.hbs +1 -0
  39. package/addon/components/cell/place-identity.hbs +1 -0
  40. package/addon/components/cell/position-identity.hbs +1 -0
  41. package/addon/components/cell/purchase-rate-identity.hbs +1 -0
  42. package/addon/components/cell/recurring-order-schedule-identity.hbs +1 -0
  43. package/addon/components/cell/resource-list.hbs +10 -0
  44. package/addon/components/cell/resource-list.js +28 -0
  45. package/addon/components/cell/route-identity.hbs +1 -0
  46. package/addon/components/cell/sensor-identity.hbs +1 -0
  47. package/addon/components/cell/service-area-identity.hbs +1 -0
  48. package/addon/components/cell/service-quote-identity.hbs +1 -0
  49. package/addon/components/cell/service-quote-item-identity.hbs +1 -0
  50. package/addon/components/cell/service-rate-fee-identity.hbs +1 -0
  51. package/addon/components/cell/service-rate-identity.hbs +1 -0
  52. package/addon/components/cell/service-rate-parcel-fee-identity.hbs +1 -0
  53. package/addon/components/cell/telematic-identity.hbs +1 -0
  54. package/addon/components/cell/tracking-number-identity.hbs +1 -0
  55. package/addon/components/cell/tracking-status-identity.hbs +1 -0
  56. package/addon/components/cell/trailer-identity.hbs +1 -0
  57. package/addon/components/cell/vehicle-device-identity.hbs +1 -0
  58. package/addon/components/cell/vehicle-identity.hbs +1 -33
  59. package/addon/components/cell/vendor-identity.hbs +1 -0
  60. package/addon/components/cell/warranty-identity.hbs +1 -0
  61. package/addon/components/cell/waypoint-identity.hbs +1 -0
  62. package/addon/components/cell/work-order-identity.hbs +1 -0
  63. package/addon/components/cell/zone-identity.hbs +1 -0
  64. package/addon/components/contact/form.hbs +2 -4
  65. package/addon/components/contact/pill.hbs +24 -0
  66. package/addon/components/contact/summary.hbs +1 -0
  67. package/addon/components/customer/create-order-form.hbs +8 -4
  68. package/addon/components/customer/form.hbs +2 -4
  69. package/addon/components/customer/order-form.hbs +10 -8
  70. package/addon/components/customer/pill.hbs +24 -0
  71. package/addon/components/customer/summary.hbs +1 -0
  72. package/addon/components/device/details.hbs +3 -1
  73. package/addon/components/device/form.hbs +4 -13
  74. package/addon/components/device/panel-header.hbs +3 -2
  75. package/addon/components/device/panel-header.js +1 -5
  76. package/addon/components/device/panel-tabs/vehicle.hbs +2 -2
  77. package/addon/components/device/pill.hbs +24 -16
  78. package/addon/components/device/summary.hbs +1 -0
  79. package/addon/components/device/telemetry.hbs +17 -0
  80. package/addon/components/device/telemetry.js +117 -0
  81. package/addon/components/device-event/details.hbs +6 -24
  82. package/addon/components/device-event/pill.hbs +24 -0
  83. package/addon/components/device-event/summary.hbs +1 -0
  84. package/addon/components/driver/details.hbs +9 -1
  85. package/addon/components/driver/form.hbs +6 -18
  86. package/addon/components/driver/panel-header.hbs +3 -4
  87. package/addon/components/driver/panel-header.js +8 -1
  88. package/addon/components/driver/pill.hbs +16 -8
  89. package/addon/components/driver/summary.hbs +1 -0
  90. package/addon/components/entity/pill.hbs +24 -0
  91. package/addon/components/entity/summary.hbs +1 -0
  92. package/addon/components/equipment/details.hbs +6 -2
  93. package/addon/components/equipment/form.hbs +10 -4
  94. package/addon/components/equipment/pill.hbs +24 -20
  95. package/addon/components/equipment/summary.hbs +1 -0
  96. package/addon/components/facilitator/pill.hbs +24 -0
  97. package/addon/components/facilitator/summary.hbs +1 -0
  98. package/addon/components/fleet/details.hbs +6 -2
  99. package/addon/components/fleet/driver-listing.hbs +3 -12
  100. package/addon/components/fleet/form.hbs +8 -4
  101. package/addon/components/fleet/pill.hbs +24 -0
  102. package/addon/components/fleet/summary.hbs +1 -0
  103. package/addon/components/fleet/vehicle-listing.hbs +3 -12
  104. package/addon/components/fleet-driver/pill.hbs +24 -0
  105. package/addon/components/fleet-driver/summary.hbs +1 -0
  106. package/addon/components/fleet-driver-listing.hbs +2 -1
  107. package/addon/components/fleet-panel/details.hbs +6 -2
  108. package/addon/components/fleet-panel/driver-listing.hbs +2 -1
  109. package/addon/components/fleet-panel/vehicle-listing.hbs +2 -1
  110. package/addon/components/fleet-vehicle-listing.hbs +2 -1
  111. package/addon/components/fuel-integration/form.hbs +125 -25
  112. package/addon/components/fuel-integration/form.js +74 -19
  113. package/addon/components/fuel-integration/hub.hbs +68 -65
  114. package/addon/components/fuel-integration/hub.js +15 -19
  115. package/addon/components/fuel-provider-connection/pill.hbs +24 -0
  116. package/addon/components/fuel-provider-connection/summary.hbs +1 -0
  117. package/addon/components/fuel-provider-transaction/pill.hbs +24 -0
  118. package/addon/components/fuel-provider-transaction/summary.hbs +1 -0
  119. package/addon/components/fuel-report/details.hbs +9 -3
  120. package/addon/components/fuel-report/form.hbs +6 -3
  121. package/addon/components/fuel-report/pill.hbs +24 -0
  122. package/addon/components/fuel-report/summary.hbs +1 -0
  123. package/addon/components/fuel-transaction-status.hbs +1 -0
  124. package/addon/components/fuel-transaction-status.js +8 -0
  125. package/addon/components/geofence-map.hbs +19 -0
  126. package/addon/components/geofence-map.js +83 -0
  127. package/addon/components/inspection-field/form.hbs +103 -0
  128. package/addon/components/inspection-field/form.js +211 -0
  129. package/addon/components/inspection-field/input.hbs +270 -0
  130. package/addon/components/inspection-field/input.js +472 -0
  131. package/addon/components/inspection-field/value.hbs +90 -0
  132. package/addon/components/inspection-field/value.js +158 -0
  133. package/addon/components/inspection-flyout.hbs +34 -0
  134. package/addon/components/inspection-flyout.js +66 -0
  135. package/addon/components/inspection-form/builder.hbs +88 -0
  136. package/addon/components/inspection-form/builder.js +211 -0
  137. package/addon/components/inspection-form/details/submissions.hbs +48 -0
  138. package/addon/components/inspection-form/details/submissions.js +42 -0
  139. package/addon/components/inspection-form/details.hbs +97 -0
  140. package/addon/components/inspection-form/details.js +57 -0
  141. package/addon/components/inspection-form/form.hbs +93 -0
  142. package/addon/components/inspection-form/form.js +79 -0
  143. package/addon/components/inspection-form/pill.hbs +24 -0
  144. package/addon/components/inspection-form/summary.hbs +1 -0
  145. package/addon/components/inspection-item-result/pill.hbs +24 -0
  146. package/addon/components/inspection-item-result/summary.hbs +1 -0
  147. package/addon/components/inspection-link/list.hbs +94 -0
  148. package/addon/components/inspection-link/list.js +129 -0
  149. package/addon/components/inspection-sheet/group.hbs +45 -0
  150. package/addon/components/inspection-sheet/group.js +75 -0
  151. package/addon/components/inspection-sheet.hbs +77 -0
  152. package/addon/components/inspection-sheet.js +152 -0
  153. package/addon/components/inspection-submission/details.hbs +149 -0
  154. package/addon/components/inspection-submission/details.js +130 -0
  155. package/addon/components/inspection-submission/form.hbs +114 -0
  156. package/addon/components/inspection-submission/form.js +129 -0
  157. package/addon/components/inspection-submission/photos.hbs +20 -0
  158. package/addon/components/inspection-submission/photos.js +57 -0
  159. package/addon/components/inspection-submission/pill.hbs +24 -0
  160. package/addon/components/inspection-submission/summary.hbs +1 -0
  161. package/addon/components/integrated-vendor/pill.hbs +24 -0
  162. package/addon/components/integrated-vendor/summary.hbs +1 -0
  163. package/addon/components/issue/details.hbs +12 -0
  164. package/addon/components/issue/details.js +36 -0
  165. package/addon/components/issue/form.hbs +10 -5
  166. package/addon/components/issue/pill.hbs +24 -0
  167. package/addon/components/issue/summary.hbs +1 -0
  168. package/addon/components/layout/fleet-ops-sidebar.js +14 -3
  169. package/addon/components/maintenance/details.hbs +9 -3
  170. package/addon/components/maintenance/form.hbs +22 -3
  171. package/addon/components/maintenance/form.js +4 -4
  172. package/addon/components/maintenance/panel-header.hbs +14 -12
  173. package/addon/components/maintenance/pill.hbs +24 -0
  174. package/addon/components/maintenance/summary.hbs +1 -0
  175. package/addon/components/maintenance-schedule/details.hbs +7 -3
  176. package/addon/components/maintenance-schedule/form.hbs +20 -2
  177. package/addon/components/maintenance-schedule/form.js +6 -6
  178. package/addon/components/maintenance-schedule/pill.hbs +24 -0
  179. package/addon/components/maintenance-schedule/summary.hbs +1 -0
  180. package/addon/components/maintenance-schedule/work-orders.hbs +39 -0
  181. package/addon/components/maintenance-schedule/work-orders.js +37 -0
  182. package/addon/components/maintenance-subject/pill.hbs +24 -0
  183. package/addon/components/maintenance-subject/summary.hbs +1 -0
  184. package/addon/components/manifest/pill.hbs +24 -0
  185. package/addon/components/manifest/summary.hbs +1 -0
  186. package/addon/components/manifest-stop/pill.hbs +24 -0
  187. package/addon/components/manifest-stop/summary.hbs +1 -0
  188. package/addon/components/map/drawer/device-event-listing.hbs +4 -14
  189. package/addon/components/map/drawer/device-event-listing.js +3 -1
  190. package/addon/components/map/drawer/driver-listing.js +6 -2
  191. package/addon/components/map/drawer/position-listing.hbs +2 -16
  192. package/addon/components/map/drawer/vehicle-listing.js +1 -2
  193. package/addon/components/modals/add-driver-shift.hbs +2 -1
  194. package/addon/components/modals/assign-driver.hbs +2 -16
  195. package/addon/components/modals/attach-device.hbs +2 -1
  196. package/addon/components/modals/attach-equipment.hbs +2 -4
  197. package/addon/components/modals/attach-telematic-device.hbs +8 -1
  198. package/addon/components/modals/attach-trailer.hbs +2 -1
  199. package/addon/components/modals/bulk-assign-driver.hbs +2 -9
  200. package/addon/components/modals/bulk-assign-orders.hbs +2 -9
  201. package/addon/components/modals/driver-assign-order.hbs +2 -6
  202. package/addon/components/modals/driver-assign-vehicle.hbs +4 -12
  203. package/addon/components/modals/driver-assign-vehicle.js +30 -0
  204. package/addon/components/modals/driver-assign-vendor.hbs +2 -1
  205. package/addon/components/modals/driver-form.hbs +4 -2
  206. package/addon/components/modals/fleet-assign-driver.hbs +2 -1
  207. package/addon/components/modals/fleet-assign-vehicle.hbs +2 -1
  208. package/addon/components/modals/fleet-form.hbs +4 -2
  209. package/addon/components/modals/fuel-connection-diagnostics.hbs +35 -0
  210. package/addon/components/modals/fuel-connection-diagnostics.js +87 -0
  211. package/addon/components/modals/fuel-report-form.hbs +4 -2
  212. package/addon/components/modals/fuel-transaction-action.hbs +48 -0
  213. package/addon/components/modals/fuel-transaction-action.js +98 -0
  214. package/addon/components/modals/group-form.hbs +2 -1
  215. package/addon/components/modals/inspection-follow-up.hbs +31 -0
  216. package/addon/components/modals/inspection-follow-up.js +39 -0
  217. package/addon/components/modals/inspection-link.hbs +100 -0
  218. package/addon/components/modals/inspection-link.js +83 -0
  219. package/addon/components/modals/issue-assign.hbs +2 -1
  220. package/addon/components/modals/issue-from.hbs +8 -4
  221. package/addon/components/modals/map-layer-form.hbs +2 -1
  222. package/addon/components/modals/order-assign-driver.hbs +2 -16
  223. package/addon/components/modals/order-form.hbs +8 -46
  224. package/addon/components/modals/order-route-form.hbs +8 -4
  225. package/addon/components/modals/place-assign-vendor.hbs +3 -2
  226. package/addon/components/modals/place-details.hbs +1 -1
  227. package/addon/components/modals/point-map.hbs +1 -1
  228. package/addon/components/modals/radar-notice.hbs +24 -0
  229. package/addon/components/modals/radar-notice.js +8 -0
  230. package/addon/components/modals/radar-save-view.hbs +7 -0
  231. package/addon/components/modals/radar-select-resource.hbs +35 -0
  232. package/addon/components/modals/radar-send-pin.hbs +20 -0
  233. package/addon/components/modals/radar-send-pin.js +8 -0
  234. package/addon/components/modals/select-trailer.hbs +2 -4
  235. package/addon/components/modals/vehicle-form.hbs +2 -1
  236. package/addon/components/modals/vendor-form.hbs +2 -3
  237. package/addon/components/order/details/activity.js +9 -0
  238. package/addon/components/order/details/detail.hbs +2 -32
  239. package/addon/components/order/form/details.hbs +10 -48
  240. package/addon/components/order/form/route.hbs +10 -12
  241. package/addon/components/order/form/service-rate.hbs +2 -3
  242. package/addon/components/order/pill.hbs +24 -34
  243. package/addon/components/order/route-editor.hbs +8 -4
  244. package/addon/components/order/schedule-card.hbs +2 -1
  245. package/addon/components/order/summary.hbs +1 -0
  246. package/addon/components/order-config/pill.hbs +24 -0
  247. package/addon/components/order-config/summary.hbs +1 -0
  248. package/addon/components/part/details.hbs +15 -3
  249. package/addon/components/part/details.js +37 -1
  250. package/addon/components/part/form.hbs +12 -5
  251. package/addon/components/part/pill.hbs +24 -0
  252. package/addon/components/part/summary.hbs +1 -0
  253. package/addon/components/payload/pill.hbs +24 -0
  254. package/addon/components/payload/summary.hbs +1 -0
  255. package/addon/components/place/details.hbs +1 -1
  256. package/addon/components/place/pill.hbs +24 -0
  257. package/addon/components/place/summary.hbs +1 -0
  258. package/addon/components/position/pill.hbs +24 -0
  259. package/addon/components/position/summary.hbs +1 -0
  260. package/addon/components/positions-replay.hbs +2 -4
  261. package/addon/components/public-inspection.hbs +174 -0
  262. package/addon/components/public-inspection.js +279 -0
  263. package/addon/components/purchase-rate/pill.hbs +24 -0
  264. package/addon/components/purchase-rate/summary.hbs +1 -0
  265. package/addon/components/radar/agenda/entry.hbs +17 -0
  266. package/addon/components/radar/agenda/entry.js +26 -0
  267. package/addon/components/radar/agenda/lane.hbs +33 -0
  268. package/addon/components/radar/agenda/lane.js +80 -0
  269. package/addon/components/radar/agenda.hbs +69 -0
  270. package/addon/components/radar/agenda.js +29 -0
  271. package/addon/components/radar/briefing.hbs +108 -0
  272. package/addon/components/radar/briefing.js +43 -0
  273. package/addon/components/radar/bulk-bar.hbs +41 -0
  274. package/addon/components/radar/bulk-bar.js +32 -0
  275. package/addon/components/radar/decision-card.hbs +28 -0
  276. package/addon/components/radar/decision-card.js +7 -0
  277. package/addon/components/radar/empty-state.hbs +30 -0
  278. package/addon/components/radar/empty-state.js +27 -0
  279. package/addon/components/radar/filter-pills.hbs +22 -0
  280. package/addon/components/radar/filter-pills.js +3 -0
  281. package/addon/components/radar/handover-card.hbs +62 -0
  282. package/addon/components/radar/handover-card.js +14 -0
  283. package/addon/components/radar/header.hbs +28 -0
  284. package/addon/components/radar/header.js +12 -0
  285. package/addon/components/radar/item-drawer.hbs +92 -0
  286. package/addon/components/radar/item-drawer.js +37 -0
  287. package/addon/components/radar/item-list.hbs +20 -0
  288. package/addon/components/radar/item-list.js +3 -0
  289. package/addon/components/radar/item-row.hbs +90 -0
  290. package/addon/components/radar/item-row.js +43 -0
  291. package/addon/components/radar/keyboard-hints.hbs +8 -0
  292. package/addon/components/radar/keyboard-hints.js +3 -0
  293. package/addon/components/radar/subject.hbs +14 -0
  294. package/addon/components/radar/subject.js +26 -0
  295. package/addon/components/radar/view-bar.hbs +27 -0
  296. package/addon/components/radar/view-bar.js +16 -0
  297. package/addon/components/recurring-order-schedule/pill.hbs +24 -0
  298. package/addon/components/recurring-order-schedule/summary.hbs +1 -0
  299. package/addon/components/resource-activity.hbs +1 -0
  300. package/addon/components/resource-activity.js +4 -0
  301. package/addon/components/route/pill.hbs +24 -0
  302. package/addon/components/route/summary.hbs +1 -0
  303. package/addon/components/select-option/asset-connection.hbs +12 -0
  304. package/addon/components/select-option/asset.hbs +12 -0
  305. package/addon/components/select-option/attachable.hbs +12 -0
  306. package/addon/components/select-option/contact.hbs +12 -0
  307. package/addon/components/select-option/customer.hbs +12 -0
  308. package/addon/components/select-option/device-event.hbs +12 -0
  309. package/addon/components/select-option/device.hbs +12 -0
  310. package/addon/components/select-option/driver.hbs +12 -0
  311. package/addon/components/select-option/entity.hbs +12 -0
  312. package/addon/components/select-option/equipment.hbs +12 -0
  313. package/addon/components/select-option/facilitator.hbs +12 -0
  314. package/addon/components/select-option/fleet-driver.hbs +12 -0
  315. package/addon/components/select-option/fleet.hbs +12 -0
  316. package/addon/components/select-option/fuel-provider-connection.hbs +12 -0
  317. package/addon/components/select-option/fuel-provider-transaction.hbs +12 -0
  318. package/addon/components/select-option/fuel-report.hbs +12 -0
  319. package/addon/components/select-option/inspection-form.hbs +12 -0
  320. package/addon/components/select-option/inspection-item-result.hbs +12 -0
  321. package/addon/components/select-option/inspection-submission.hbs +12 -0
  322. package/addon/components/select-option/integrated-vendor.hbs +12 -0
  323. package/addon/components/select-option/issue.hbs +12 -0
  324. package/addon/components/select-option/maintenance-schedule.hbs +12 -0
  325. package/addon/components/select-option/maintenance-subject.hbs +12 -0
  326. package/addon/components/select-option/maintenance.hbs +12 -0
  327. package/addon/components/select-option/manifest-stop.hbs +12 -0
  328. package/addon/components/select-option/manifest.hbs +12 -0
  329. package/addon/components/select-option/order-config.hbs +12 -0
  330. package/addon/components/select-option/order.hbs +12 -0
  331. package/addon/components/select-option/part.hbs +12 -0
  332. package/addon/components/select-option/payload.hbs +12 -0
  333. package/addon/components/select-option/place.hbs +12 -0
  334. package/addon/components/select-option/position.hbs +12 -0
  335. package/addon/components/select-option/purchase-rate.hbs +12 -0
  336. package/addon/components/select-option/recurring-order-schedule.hbs +12 -0
  337. package/addon/components/select-option/route.hbs +12 -0
  338. package/addon/components/select-option/sensor.hbs +12 -0
  339. package/addon/components/select-option/service-area.hbs +12 -0
  340. package/addon/components/select-option/service-quote-item.hbs +12 -0
  341. package/addon/components/select-option/service-quote.hbs +12 -0
  342. package/addon/components/select-option/service-rate-fee.hbs +12 -0
  343. package/addon/components/select-option/service-rate-parcel-fee.hbs +12 -0
  344. package/addon/components/select-option/service-rate.hbs +12 -0
  345. package/addon/components/select-option/telematic.hbs +12 -0
  346. package/addon/components/select-option/trackable.hbs +8 -0
  347. package/addon/components/select-option/tracking-number.hbs +12 -0
  348. package/addon/components/select-option/tracking-status.hbs +12 -0
  349. package/addon/components/select-option/trailer.hbs +12 -0
  350. package/addon/components/select-option/vehicle-device.hbs +12 -0
  351. package/addon/components/select-option/vehicle.hbs +12 -0
  352. package/addon/components/select-option/vendor.hbs +12 -0
  353. package/addon/components/select-option/warranty.hbs +12 -0
  354. package/addon/components/select-option/waypoint.hbs +12 -0
  355. package/addon/components/select-option/work-order.hbs +12 -0
  356. package/addon/components/select-option/zone.hbs +12 -0
  357. package/addon/components/sensor/details.hbs +6 -2
  358. package/addon/components/sensor/form.hbs +6 -17
  359. package/addon/components/sensor/panel-header.hbs +1 -1
  360. package/addon/components/sensor/pill.hbs +24 -0
  361. package/addon/components/sensor/summary.hbs +1 -0
  362. package/addon/components/service-area/details.hbs +1 -20
  363. package/addon/components/service-area/pill.hbs +24 -0
  364. package/addon/components/service-area/summary.hbs +1 -0
  365. package/addon/components/service-quote/pill.hbs +24 -0
  366. package/addon/components/service-quote/summary.hbs +1 -0
  367. package/addon/components/service-quote-item/pill.hbs +24 -0
  368. package/addon/components/service-quote-item/summary.hbs +1 -0
  369. package/addon/components/service-rate/details.hbs +12 -6
  370. package/addon/components/service-rate/form.hbs +10 -8
  371. package/addon/components/service-rate/pill.hbs +24 -0
  372. package/addon/components/service-rate/summary.hbs +1 -0
  373. package/addon/components/service-rate-fee/pill.hbs +24 -0
  374. package/addon/components/service-rate-fee/summary.hbs +1 -0
  375. package/addon/components/service-rate-parcel-fee/pill.hbs +24 -0
  376. package/addon/components/service-rate-parcel-fee/summary.hbs +1 -0
  377. package/addon/components/table/cell/fleet-ops-option.hbs +20 -0
  378. package/addon/components/telematic/details.hbs +4 -1
  379. package/addon/components/telematic/details.js +1 -0
  380. package/addon/components/telematic/form.hbs +6 -1
  381. package/addon/components/telematic/form.js +1 -0
  382. package/addon/components/telematic/pill.hbs +24 -0
  383. package/addon/components/telematic/settings.hbs +6 -1
  384. package/addon/components/telematic/settings.js +1 -0
  385. package/addon/components/telematic/summary.hbs +1 -0
  386. package/addon/components/telematic/telemetry-status.hbs +43 -0
  387. package/addon/components/telematic/telemetry-status.js +95 -0
  388. package/addon/components/tracking-number/pill.hbs +24 -0
  389. package/addon/components/tracking-number/summary.hbs +1 -0
  390. package/addon/components/tracking-status/pill.hbs +24 -0
  391. package/addon/components/tracking-status/summary.hbs +1 -0
  392. package/addon/components/trailer/card.hbs +2 -5
  393. package/addon/components/trailer/details.hbs +11 -5
  394. package/addon/components/trailer/details.js +4 -0
  395. package/addon/components/trailer/form.hbs +6 -3
  396. package/addon/components/trailer/pill.hbs +24 -21
  397. package/addon/components/trailer/summary.hbs +1 -0
  398. package/addon/components/vehicle/card.hbs +4 -8
  399. package/addon/components/vehicle/details/inspections.hbs +49 -0
  400. package/addon/components/vehicle/details/inspections.js +44 -0
  401. package/addon/components/vehicle/details/maintenance-history.hbs +1 -1
  402. package/addon/components/vehicle/details/schedules.hbs +2 -2
  403. package/addon/components/vehicle/details/work-orders.hbs +1 -1
  404. package/addon/components/vehicle/details.hbs +18 -1
  405. package/addon/components/vehicle/form.hbs +2 -1
  406. package/addon/components/vehicle/panel-header.hbs +3 -4
  407. package/addon/components/vehicle/panel-header.js +8 -1
  408. package/addon/components/vehicle/pill.hbs +15 -24
  409. package/addon/components/vehicle/summary.hbs +1 -0
  410. package/addon/components/vehicle-device/pill.hbs +24 -0
  411. package/addon/components/vehicle-device/summary.hbs +1 -0
  412. package/addon/components/vendor/form.hbs +2 -4
  413. package/addon/components/vendor/personnel-manager.hbs +2 -9
  414. package/addon/components/vendor/personnel-tab.hbs +1 -0
  415. package/addon/components/vendor/personnel-tab.js +4 -0
  416. package/addon/components/vendor/pill.hbs +24 -0
  417. package/addon/components/vendor/summary.hbs +1 -0
  418. package/addon/components/warranty/details.hbs +3 -1
  419. package/addon/components/warranty/form.hbs +2 -1
  420. package/addon/components/warranty/pill.hbs +24 -0
  421. package/addon/components/warranty/summary.hbs +1 -0
  422. package/addon/components/waypoint/pill.hbs +24 -0
  423. package/addon/components/waypoint/summary.hbs +1 -0
  424. package/addon/components/widget/radar.hbs +28 -0
  425. package/addon/components/widget/radar.js +44 -0
  426. package/addon/components/work-order/details.hbs +20 -4
  427. package/addon/components/work-order/details.js +64 -1
  428. package/addon/components/work-order/form.hbs +20 -2
  429. package/addon/components/work-order/form.js +10 -10
  430. package/addon/components/work-order/panel-header.hbs +15 -13
  431. package/addon/components/work-order/pill.hbs +24 -0
  432. package/addon/components/work-order/summary.hbs +1 -0
  433. package/addon/components/zone/details.hbs +2 -21
  434. package/addon/components/zone/pill.hbs +24 -0
  435. package/addon/components/zone/summary.hbs +1 -0
  436. package/addon/controllers/connectivity/devices/index/details/sensors.js +4 -0
  437. package/addon/controllers/connectivity/devices/index/details.js +1 -18
  438. package/addon/controllers/connectivity/events/index.js +1 -4
  439. package/addon/controllers/connectivity/fuel-providers/details/index.js +6 -0
  440. package/addon/controllers/connectivity/fuel-providers/details/matching.js +13 -0
  441. package/addon/controllers/connectivity/fuel-providers/details/sync.js +21 -10
  442. package/addon/controllers/connectivity/fuel-providers/details/transactions.js +5 -16
  443. package/addon/controllers/connectivity/fuel-providers/details.js +76 -13
  444. package/addon/controllers/connectivity/fuel-providers/index.js +22 -17
  445. package/addon/controllers/connectivity/sensors/index.js +1 -3
  446. package/addon/controllers/connectivity/telematics/details/events.js +8 -0
  447. package/addon/controllers/connectivity/telematics/details/sensors.js +3 -2
  448. package/addon/controllers/connectivity/telematics/index.js +3 -1
  449. package/addon/controllers/maintenance/inspection-forms/index/details.js +29 -0
  450. package/addon/controllers/maintenance/inspection-forms/index/edit.js +39 -0
  451. package/addon/controllers/maintenance/inspection-forms/index/new.js +48 -0
  452. package/addon/controllers/maintenance/inspection-forms/index.js +90 -0
  453. package/addon/controllers/maintenance/inspection-submissions/index/details.js +53 -0
  454. package/addon/controllers/maintenance/inspection-submissions/index/edit.js +39 -0
  455. package/addon/controllers/maintenance/inspection-submissions/index/new.js +48 -0
  456. package/addon/controllers/maintenance/inspection-submissions/index.js +142 -0
  457. package/addon/controllers/maintenance/maintenances/index.js +6 -2
  458. package/addon/controllers/maintenance/schedules/index/details.js +4 -86
  459. package/addon/controllers/maintenance/schedules/index.js +15 -0
  460. package/addon/controllers/maintenance/work-orders/index.js +19 -1
  461. package/addon/controllers/management/contacts/customers.js +6 -4
  462. package/addon/controllers/management/contacts/index.js +6 -3
  463. package/addon/controllers/management/drivers/index/details.js +1 -66
  464. package/addon/controllers/management/drivers/index.js +9 -34
  465. package/addon/controllers/management/fleets/index/details.js +2 -15
  466. package/addon/controllers/management/fleets/index.js +12 -20
  467. package/addon/controllers/management/fuel-reports/index.js +9 -59
  468. package/addon/controllers/management/fuel-transactions/index/details.js +51 -48
  469. package/addon/controllers/management/fuel-transactions/index.js +32 -32
  470. package/addon/controllers/management/index.js +1367 -49
  471. package/addon/controllers/management/issues/index/details.js +1 -33
  472. package/addon/controllers/management/issues/index.js +10 -59
  473. package/addon/controllers/management/trailers/index/details.js +4 -78
  474. package/addon/controllers/management/trailers/index.js +20 -22
  475. package/addon/controllers/management/vehicles/index/details.js +9 -77
  476. package/addon/controllers/management/vehicles/index.js +50 -72
  477. package/addon/controllers/management/vendors/index.js +6 -3
  478. package/addon/controllers/operations/orders/index.js +38 -11
  479. package/addon/controllers/operations/service-rates/index.js +10 -2
  480. package/addon/controllers/settings/payments/index.js +11 -2
  481. package/addon/extension.js +36 -0
  482. package/addon/helpers/initials.js +6 -0
  483. package/addon/helpers/leaflet-tile-url.js +6 -1
  484. package/addon/instance-initializers/register-fleetops-resource-descriptors.js +29 -0
  485. package/addon/modifiers/inspection-flyout.js +237 -0
  486. package/addon/modifiers/radar-keyboard.js +78 -0
  487. package/addon/modifiers/sync-value.js +26 -0
  488. package/addon/modifiers/when-changed.js +25 -0
  489. package/addon/routes/connectivity/fuel-providers/details/settings.js +5 -2
  490. package/addon/routes/connectivity/fuel-providers/details/sync.js +6 -13
  491. package/addon/routes/connectivity/fuel-providers/details/transactions.js +8 -7
  492. package/addon/routes/connectivity/fuel-providers/details.js +15 -2
  493. package/addon/routes/connectivity/fuel-providers/edit.js +2 -1
  494. package/addon/routes/connectivity/fuel-providers/index.js +5 -0
  495. package/addon/routes/maintenance/inspection-forms/index/details/index.js +3 -0
  496. package/addon/routes/maintenance/inspection-forms/index/details/submissions.js +3 -0
  497. package/addon/routes/maintenance/inspection-forms/index/details.js +18 -0
  498. package/addon/routes/maintenance/inspection-forms/index/edit.js +18 -0
  499. package/addon/routes/maintenance/inspection-forms/index/new.js +3 -0
  500. package/addon/routes/maintenance/inspection-forms/index.js +22 -0
  501. package/addon/routes/maintenance/inspection-forms.js +3 -0
  502. package/addon/routes/maintenance/inspection-submissions/index/details/audit.js +8 -0
  503. package/addon/routes/maintenance/inspection-submissions/index/details/index.js +3 -0
  504. package/addon/routes/maintenance/inspection-submissions/index/details/photos.js +8 -0
  505. package/addon/routes/maintenance/inspection-submissions/index/details.js +18 -0
  506. package/addon/routes/maintenance/inspection-submissions/index/edit.js +18 -0
  507. package/addon/routes/maintenance/inspection-submissions/index/new.js +3 -0
  508. package/addon/routes/maintenance/inspection-submissions/index.js +25 -0
  509. package/addon/routes/maintenance/inspection-submissions.js +3 -0
  510. package/addon/routes/management/fuel-transactions/index/details.js +12 -2
  511. package/addon/routes/management/fuel-transactions/index.js +6 -0
  512. package/addon/routes/management/index.js +17 -1
  513. package/addon/routes/management/vehicles/index/details/inspections.js +3 -0
  514. package/addon/routes/management/vehicles/index.js +2 -2
  515. package/addon/routes.js +24 -0
  516. package/addon/services/contact-actions.js +22 -4
  517. package/addon/services/customer-actions.js +7 -4
  518. package/addon/services/device-actions.js +28 -0
  519. package/addon/services/driver-actions.js +79 -16
  520. package/addon/services/equipment-actions.js +15 -6
  521. package/addon/services/fleet-actions.js +20 -4
  522. package/addon/services/fuel-report-actions.js +7 -7
  523. package/addon/services/inspection-form-actions.js +236 -0
  524. package/addon/services/inspection-submission-actions.js +273 -0
  525. package/addon/services/issue-actions.js +58 -7
  526. package/addon/services/maintenance-actions.js +22 -3
  527. package/addon/services/maintenance-schedule-actions.js +70 -5
  528. package/addon/services/movement-tracker.js +52 -1
  529. package/addon/services/part-actions.js +13 -7
  530. package/addon/services/place-actions.js +8 -7
  531. package/addon/services/service-area-actions.js +27 -4
  532. package/addon/services/trailer-actions.js +60 -7
  533. package/addon/services/vehicle-actions.js +58 -3
  534. package/addon/services/vendor-actions.js +8 -7
  535. package/addon/services/work-order-actions.js +14 -5
  536. package/addon/services/zone-actions.js +27 -4
  537. package/addon/styles/fleetops-engine.css +3621 -75
  538. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -2
  539. package/addon/templates/connectivity/events/details.hbs +2 -2
  540. package/addon/templates/connectivity/fuel-providers/details/index.hbs +65 -35
  541. package/addon/templates/connectivity/fuel-providers/details/matching.hbs +2 -2
  542. package/addon/templates/connectivity/fuel-providers/details/sync.hbs +54 -43
  543. package/addon/templates/connectivity/fuel-providers/details/transactions.hbs +10 -5
  544. package/addon/templates/connectivity/fuel-providers/details.hbs +38 -13
  545. package/addon/templates/connectivity/fuel-providers/edit.hbs +1 -0
  546. package/addon/templates/connectivity/fuel-providers/index.hbs +2 -1
  547. package/addon/templates/connectivity/fuel-providers/new.hbs +1 -1
  548. package/addon/templates/connectivity/telematics/details/attachments.hbs +2 -1
  549. package/addon/templates/maintenance/inspection-forms/index/details/index.hbs +1 -0
  550. package/addon/templates/maintenance/inspection-forms/index/details/submissions.hbs +2 -0
  551. package/addon/templates/maintenance/inspection-forms/index/details.hbs +14 -0
  552. package/addon/templates/maintenance/inspection-forms/index/edit.hbs +11 -0
  553. package/addon/templates/maintenance/inspection-forms/index/new.hbs +11 -0
  554. package/addon/templates/maintenance/inspection-forms/index.hbs +28 -0
  555. package/addon/templates/maintenance/inspection-forms.hbs +1 -0
  556. package/addon/templates/maintenance/inspection-submissions/index/details/audit.hbs +1 -0
  557. package/addon/templates/maintenance/inspection-submissions/index/details/index.hbs +1 -0
  558. package/addon/templates/maintenance/inspection-submissions/index/details/photos.hbs +1 -0
  559. package/addon/templates/maintenance/inspection-submissions/index/details.hbs +14 -0
  560. package/addon/templates/maintenance/inspection-submissions/index/edit.hbs +11 -0
  561. package/addon/templates/maintenance/inspection-submissions/index/new.hbs +11 -0
  562. package/addon/templates/maintenance/inspection-submissions/index.hbs +28 -0
  563. package/addon/templates/maintenance/inspection-submissions.hbs +1 -0
  564. package/addon/templates/management/fuel-transactions/index/details.hbs +64 -62
  565. package/addon/templates/management/fuel-transactions/index.hbs +9 -31
  566. package/addon/templates/management/index.hbs +110 -102
  567. package/addon/templates/management/vehicles/index/details/inspections.hbs +2 -0
  568. package/addon/templates/operations/orders/index.hbs +2 -4
  569. package/addon/utils/context-panel.js +34 -0
  570. package/addon/utils/device-table-columns.js +18 -27
  571. package/addon/utils/fleet-ops-options.js +40 -0
  572. package/addon/utils/fuel-integration-format.js +43 -0
  573. package/addon/utils/fuel-transaction.js +115 -0
  574. package/addon/utils/identity-cell-resource.js +56 -0
  575. package/addon/utils/inspection-answers.js +283 -0
  576. package/addon/utils/inspection-field-types.js +74 -0
  577. package/addon/utils/inspection-form-structure.js +161 -0
  578. package/addon/utils/radar.js +212 -0
  579. package/addon/utils/relation-value.js +37 -0
  580. package/addon/utils/resource-descriptors/assets.js +246 -0
  581. package/addon/utils/resource-descriptors/connectivity.js +146 -0
  582. package/addon/utils/resource-descriptors/fuel.js +75 -0
  583. package/addon/utils/resource-descriptors/helpers.js +191 -0
  584. package/addon/utils/resource-descriptors/index.js +35 -0
  585. package/addon/utils/resource-descriptors/maintenance.js +175 -0
  586. package/addon/utils/resource-descriptors/operations.js +215 -0
  587. package/addon/utils/resource-descriptors/people.js +173 -0
  588. package/addon/utils/resource-descriptors/polymorphic.js +123 -0
  589. package/addon/utils/resource-descriptors/sub-records.js +297 -0
  590. package/addon/utils/telemetry-timestamp.js +6 -0
  591. package/app/components/asset/pill.js +1 -0
  592. package/app/components/asset/summary.js +1 -0
  593. package/app/components/asset-connection/pill.js +1 -0
  594. package/app/components/asset-connection/summary.js +1 -0
  595. package/app/components/attachable/pill.js +1 -0
  596. package/app/components/attachable/summary.js +1 -0
  597. package/app/components/cell/asset-connection-identity.js +1 -0
  598. package/app/components/cell/{attached-vehicle.js → asset-identity.js} +1 -1
  599. package/app/components/cell/attachable-identity.js +1 -0
  600. package/app/components/cell/{telematic-device.js → contact-identity.js} +1 -1
  601. package/app/components/cell/customer-identity.js +1 -0
  602. package/app/components/cell/device-event-identity.js +1 -0
  603. package/app/components/cell/entity-identity.js +1 -0
  604. package/app/components/cell/facilitator-identity.js +1 -0
  605. package/app/components/cell/fleet-driver-identity.js +1 -0
  606. package/app/components/cell/fleet-identity.js +1 -0
  607. package/app/components/cell/fuel-provider-connection-identity.js +1 -0
  608. package/app/components/cell/fuel-provider-transaction-identity.js +1 -0
  609. package/app/components/cell/fuel-report-identity.js +1 -0
  610. package/app/components/cell/inspection-form-identity.js +1 -0
  611. package/app/components/cell/inspection-item-result-identity.js +1 -0
  612. package/app/components/cell/inspection-submission-identity.js +1 -0
  613. package/app/components/cell/integrated-vendor-identity.js +1 -0
  614. package/app/components/cell/issue-identity.js +1 -0
  615. package/app/components/cell/maintenance-identity.js +1 -0
  616. package/app/components/cell/maintenance-schedule-identity.js +1 -0
  617. package/app/components/cell/maintenance-subject-identity.js +1 -0
  618. package/app/components/cell/manifest-identity.js +1 -0
  619. package/app/components/cell/manifest-stop-identity.js +1 -0
  620. package/app/components/cell/order-config-identity.js +1 -0
  621. package/app/components/cell/order-identity.js +1 -0
  622. package/app/components/cell/payload-identity.js +1 -0
  623. package/app/components/cell/place-identity.js +1 -0
  624. package/app/components/cell/position-identity.js +1 -0
  625. package/app/components/cell/purchase-rate-identity.js +1 -0
  626. package/app/components/cell/recurring-order-schedule-identity.js +1 -0
  627. package/app/components/cell/resource-list.js +1 -0
  628. package/app/components/cell/route-identity.js +1 -0
  629. package/app/components/cell/sensor-identity.js +1 -0
  630. package/app/components/cell/service-area-identity.js +1 -0
  631. package/app/components/cell/service-quote-identity.js +1 -0
  632. package/app/components/cell/service-quote-item-identity.js +1 -0
  633. package/app/components/cell/service-rate-fee-identity.js +1 -0
  634. package/app/components/cell/service-rate-identity.js +1 -0
  635. package/app/components/cell/service-rate-parcel-fee-identity.js +1 -0
  636. package/app/components/cell/{telematic-provider.js → telematic-identity.js} +1 -1
  637. package/app/components/cell/tracking-number-identity.js +1 -0
  638. package/app/components/cell/tracking-status-identity.js +1 -0
  639. package/app/components/cell/trailer-identity.js +1 -0
  640. package/app/components/cell/vehicle-device-identity.js +1 -0
  641. package/app/components/cell/vendor-identity.js +1 -0
  642. package/app/components/cell/warranty-identity.js +1 -0
  643. package/app/components/cell/waypoint-identity.js +1 -0
  644. package/app/components/cell/work-order-identity.js +1 -0
  645. package/app/components/cell/zone-identity.js +1 -0
  646. package/app/components/{cell/driver-name.js → contact/pill.js} +1 -1
  647. package/app/components/{cell/vehicle-name.js → contact/summary.js} +1 -1
  648. package/app/components/customer/pill.js +1 -0
  649. package/app/components/customer/summary.js +1 -0
  650. package/app/components/device/summary.js +1 -0
  651. package/app/components/device/telemetry.js +1 -0
  652. package/app/components/device-event/pill.js +1 -0
  653. package/app/components/device-event/summary.js +1 -0
  654. package/app/components/driver/summary.js +1 -0
  655. package/app/components/entity/pill.js +1 -0
  656. package/app/components/entity/summary.js +1 -0
  657. package/app/components/equipment/card.js +1 -0
  658. package/app/components/equipment/panel-header.js +1 -0
  659. package/app/components/equipment/summary.js +1 -0
  660. package/app/components/{filter/multi-model.js → facilitator/pill.js} +1 -1
  661. package/app/components/facilitator/summary.js +1 -0
  662. package/app/components/fleet/pill.js +1 -0
  663. package/app/components/fleet/summary.js +1 -0
  664. package/app/components/fleet-driver/pill.js +1 -0
  665. package/app/components/fleet-driver/summary.js +1 -0
  666. package/app/components/fuel-provider-connection/pill.js +1 -0
  667. package/app/components/fuel-provider-connection/summary.js +1 -0
  668. package/app/components/fuel-provider-transaction/pill.js +1 -0
  669. package/app/components/fuel-provider-transaction/summary.js +1 -0
  670. package/app/components/fuel-report/pill.js +1 -0
  671. package/app/components/fuel-report/summary.js +1 -0
  672. package/app/components/fuel-transaction-status.js +1 -0
  673. package/app/components/geofence-map.js +1 -0
  674. package/app/components/inspection-field/form.js +1 -0
  675. package/app/components/inspection-field/input.js +1 -0
  676. package/app/components/inspection-field/value.js +1 -0
  677. package/app/components/inspection-flyout.js +1 -0
  678. package/app/components/inspection-form/builder.js +1 -0
  679. package/app/components/inspection-form/details/submissions.js +1 -0
  680. package/app/components/inspection-form/details.js +1 -0
  681. package/app/components/inspection-form/form.js +1 -0
  682. package/app/components/inspection-form/pill.js +1 -0
  683. package/app/components/inspection-form/summary.js +1 -0
  684. package/app/components/inspection-item-result/pill.js +1 -0
  685. package/app/components/inspection-item-result/summary.js +1 -0
  686. package/app/components/inspection-link/list.js +1 -0
  687. package/app/components/inspection-sheet/group.js +1 -0
  688. package/app/components/inspection-sheet.js +1 -0
  689. package/app/components/inspection-submission/details.js +1 -0
  690. package/app/components/inspection-submission/form.js +1 -0
  691. package/app/components/inspection-submission/photos.js +1 -0
  692. package/app/components/inspection-submission/pill.js +1 -0
  693. package/app/components/inspection-submission/summary.js +1 -0
  694. package/app/components/integrated-vendor/pill.js +1 -0
  695. package/app/components/integrated-vendor/summary.js +1 -0
  696. package/app/components/issue/pill.js +1 -0
  697. package/app/components/issue/summary.js +1 -0
  698. package/app/components/issue/timeline.js +1 -0
  699. package/app/components/layout/fleet-ops-sidebar/operations-monitor.js +1 -0
  700. package/app/components/maintenance/pill.js +1 -0
  701. package/app/components/maintenance/summary.js +1 -0
  702. package/app/components/maintenance-schedule/pill.js +1 -0
  703. package/app/components/maintenance-schedule/summary.js +1 -0
  704. package/app/components/maintenance-schedule/work-orders.js +1 -0
  705. package/app/components/maintenance-subject/pill.js +1 -0
  706. package/app/components/maintenance-subject/summary.js +1 -0
  707. package/app/components/manifest/pill.js +1 -0
  708. package/app/components/manifest/summary.js +1 -0
  709. package/app/components/manifest-stop/pill.js +1 -0
  710. package/app/components/manifest-stop/summary.js +1 -0
  711. package/app/components/modals/fuel-connection-diagnostics.js +1 -0
  712. package/app/components/modals/fuel-transaction-action.js +1 -0
  713. package/app/components/modals/inspection-follow-up.js +1 -0
  714. package/app/components/modals/inspection-link.js +1 -0
  715. package/app/components/modals/radar-notice.js +1 -0
  716. package/app/components/modals/radar-save-view.js +1 -0
  717. package/app/components/modals/radar-select-resource.js +1 -0
  718. package/app/components/modals/radar-send-pin.js +1 -0
  719. package/app/components/order/summary.js +1 -0
  720. package/app/components/order-config/pill.js +1 -0
  721. package/app/components/order-config/summary.js +1 -0
  722. package/app/components/part/card.js +1 -0
  723. package/app/components/part/panel-header.js +1 -0
  724. package/app/components/part/pill.js +1 -0
  725. package/app/components/part/summary.js +1 -0
  726. package/app/components/payload/pill.js +1 -0
  727. package/app/components/payload/summary.js +1 -0
  728. package/app/components/place/pill.js +1 -0
  729. package/app/components/place/summary.js +1 -0
  730. package/app/components/position/pill.js +1 -0
  731. package/app/components/position/summary.js +1 -0
  732. package/app/components/public-inspection.js +1 -0
  733. package/app/components/purchase-rate/pill.js +1 -0
  734. package/app/components/purchase-rate/summary.js +1 -0
  735. package/app/components/radar/agenda/entry.js +1 -0
  736. package/app/components/radar/agenda/lane.js +1 -0
  737. package/app/components/radar/agenda.js +1 -0
  738. package/app/components/radar/briefing.js +1 -0
  739. package/app/components/radar/bulk-bar.js +1 -0
  740. package/app/components/radar/decision-card.js +1 -0
  741. package/app/components/radar/empty-state.js +1 -0
  742. package/app/components/radar/filter-pills.js +1 -0
  743. package/app/components/radar/handover-card.js +1 -0
  744. package/app/components/radar/header.js +1 -0
  745. package/app/components/radar/item-drawer.js +1 -0
  746. package/app/components/radar/item-list.js +1 -0
  747. package/app/components/radar/item-row.js +1 -0
  748. package/app/components/radar/keyboard-hints.js +1 -0
  749. package/app/components/radar/subject.js +1 -0
  750. package/app/components/radar/view-bar.js +1 -0
  751. package/app/components/recurring-order-schedule/pill.js +1 -0
  752. package/app/components/recurring-order-schedule/summary.js +1 -0
  753. package/app/components/resource-activity.js +1 -0
  754. package/app/components/route/pill.js +1 -0
  755. package/app/components/route/summary.js +1 -0
  756. package/app/components/select-option/asset-connection.js +1 -0
  757. package/app/components/select-option/asset.js +1 -0
  758. package/app/components/select-option/attachable.js +1 -0
  759. package/app/components/select-option/contact.js +1 -0
  760. package/app/components/select-option/customer.js +1 -0
  761. package/app/components/select-option/device-event.js +1 -0
  762. package/app/components/select-option/device.js +1 -0
  763. package/app/components/select-option/driver.js +1 -0
  764. package/app/components/select-option/entity.js +1 -0
  765. package/app/components/select-option/equipment.js +1 -0
  766. package/app/components/select-option/facilitator.js +1 -0
  767. package/app/components/select-option/fleet-driver.js +1 -0
  768. package/app/components/select-option/fleet.js +1 -0
  769. package/app/components/select-option/fuel-provider-connection.js +1 -0
  770. package/app/components/select-option/fuel-provider-transaction.js +1 -0
  771. package/app/components/select-option/fuel-report.js +1 -0
  772. package/app/components/select-option/inspection-form.js +1 -0
  773. package/app/components/select-option/inspection-item-result.js +1 -0
  774. package/app/components/select-option/inspection-submission.js +1 -0
  775. package/app/components/select-option/integrated-vendor.js +1 -0
  776. package/app/components/select-option/issue.js +1 -0
  777. package/app/components/select-option/maintenance-schedule.js +1 -0
  778. package/app/components/select-option/maintenance-subject.js +1 -0
  779. package/app/components/select-option/maintenance.js +1 -0
  780. package/app/components/select-option/manifest-stop.js +1 -0
  781. package/app/components/select-option/manifest.js +1 -0
  782. package/app/components/select-option/order-config.js +1 -0
  783. package/app/components/select-option/order.js +1 -0
  784. package/app/components/select-option/part.js +1 -0
  785. package/app/components/select-option/payload.js +1 -0
  786. package/app/components/select-option/place.js +1 -0
  787. package/app/components/select-option/position.js +1 -0
  788. package/app/components/select-option/purchase-rate.js +1 -0
  789. package/app/components/select-option/recurring-order-schedule.js +1 -0
  790. package/app/components/select-option/route.js +1 -0
  791. package/app/components/select-option/sensor.js +1 -0
  792. package/app/components/select-option/service-area.js +1 -0
  793. package/app/components/select-option/service-quote-item.js +1 -0
  794. package/app/components/select-option/service-quote.js +1 -0
  795. package/app/components/select-option/service-rate-fee.js +1 -0
  796. package/app/components/select-option/service-rate-parcel-fee.js +1 -0
  797. package/app/components/select-option/service-rate.js +1 -0
  798. package/app/components/select-option/telematic.js +1 -0
  799. package/app/components/select-option/trackable.js +1 -0
  800. package/app/components/select-option/tracking-number.js +1 -0
  801. package/app/components/select-option/tracking-status.js +1 -0
  802. package/app/components/select-option/trailer.js +1 -0
  803. package/app/components/select-option/vehicle-device.js +1 -0
  804. package/app/components/select-option/vehicle.js +1 -0
  805. package/app/components/select-option/vendor.js +1 -0
  806. package/app/components/select-option/warranty.js +1 -0
  807. package/app/components/select-option/waypoint.js +1 -0
  808. package/app/components/select-option/work-order.js +1 -0
  809. package/app/components/select-option/zone.js +1 -0
  810. package/app/components/sensor/pill.js +1 -0
  811. package/app/components/sensor/summary.js +1 -0
  812. package/app/components/service-area/pill.js +1 -0
  813. package/app/components/service-area/summary.js +1 -0
  814. package/app/components/service-quote/pill.js +1 -0
  815. package/app/components/service-quote/summary.js +1 -0
  816. package/app/components/service-quote-item/pill.js +1 -0
  817. package/app/components/service-quote-item/summary.js +1 -0
  818. package/app/components/service-rate/pill.js +1 -0
  819. package/app/components/service-rate/summary.js +1 -0
  820. package/app/components/service-rate-fee/pill.js +1 -0
  821. package/app/components/service-rate-fee/summary.js +1 -0
  822. package/app/components/service-rate-parcel-fee/pill.js +1 -0
  823. package/app/components/service-rate-parcel-fee/summary.js +1 -0
  824. package/app/components/table/cell/fleet-ops-option.js +1 -0
  825. package/app/components/telematic/pill.js +1 -0
  826. package/app/components/telematic/summary.js +1 -0
  827. package/app/components/telematic/telemetry-status.js +1 -0
  828. package/app/components/tracking-number/pill.js +1 -0
  829. package/app/components/tracking-number/summary.js +1 -0
  830. package/app/components/tracking-status/pill.js +1 -0
  831. package/app/components/tracking-status/summary.js +1 -0
  832. package/app/components/trailer/summary.js +1 -0
  833. package/app/components/vehicle/details/inspections.js +1 -0
  834. package/app/components/vehicle/summary.js +1 -0
  835. package/app/components/vehicle-device/pill.js +1 -0
  836. package/app/components/vehicle-device/summary.js +1 -0
  837. package/app/components/vendor/personnel-tab.js +1 -0
  838. package/app/components/vendor/pill.js +1 -0
  839. package/app/components/vendor/summary.js +1 -0
  840. package/app/components/warranty/pill.js +1 -0
  841. package/app/components/warranty/summary.js +1 -0
  842. package/app/components/waypoint/pill.js +1 -0
  843. package/app/components/waypoint/summary.js +1 -0
  844. package/app/components/widget/radar.js +1 -0
  845. package/app/components/work-order/panel-header.js +1 -0
  846. package/app/components/work-order/pill.js +1 -0
  847. package/app/components/work-order/summary.js +1 -0
  848. package/app/components/zone/pill.js +1 -0
  849. package/app/components/zone/summary.js +1 -0
  850. package/app/controllers/maintenance/inspection-forms/index/details.js +1 -0
  851. package/app/controllers/maintenance/inspection-forms/index/edit.js +1 -0
  852. package/app/controllers/maintenance/inspection-forms/index/new.js +1 -0
  853. package/app/controllers/maintenance/inspection-forms/index.js +1 -0
  854. package/app/controllers/maintenance/inspection-submissions/index/details.js +1 -0
  855. package/app/controllers/maintenance/inspection-submissions/index/edit.js +1 -0
  856. package/app/controllers/maintenance/inspection-submissions/index/new.js +1 -0
  857. package/app/controllers/maintenance/inspection-submissions/index.js +1 -0
  858. package/app/helpers/initials.js +1 -0
  859. package/app/modifiers/inspection-flyout.js +1 -0
  860. package/app/modifiers/radar-keyboard.js +1 -0
  861. package/app/modifiers/sync-value.js +1 -0
  862. package/app/modifiers/when-changed.js +1 -0
  863. package/app/routes/maintenance/inspection-forms/index/details/index.js +1 -0
  864. package/app/routes/maintenance/inspection-forms/index/details/submissions.js +1 -0
  865. package/app/routes/maintenance/inspection-forms/index/details.js +1 -0
  866. package/app/routes/maintenance/inspection-forms/index/edit.js +1 -0
  867. package/app/routes/maintenance/inspection-forms/index/new.js +1 -0
  868. package/app/routes/maintenance/inspection-forms/index.js +1 -0
  869. package/app/routes/maintenance/inspection-forms.js +1 -0
  870. package/app/routes/maintenance/inspection-submissions/index/details/audit.js +1 -0
  871. package/app/routes/maintenance/inspection-submissions/index/details/index.js +1 -0
  872. package/app/routes/maintenance/inspection-submissions/index/details/photos.js +1 -0
  873. package/app/routes/maintenance/inspection-submissions/index/details.js +1 -0
  874. package/app/routes/maintenance/inspection-submissions/index/edit.js +1 -0
  875. package/app/routes/maintenance/inspection-submissions/index/new.js +1 -0
  876. package/app/routes/maintenance/inspection-submissions/index.js +1 -0
  877. package/app/routes/maintenance/inspection-submissions.js +1 -0
  878. package/app/routes/management/vehicles/index/details/inspections.js +1 -0
  879. package/app/services/inspection-form-actions.js +1 -0
  880. package/app/services/inspection-submission-actions.js +1 -0
  881. package/app/templates/maintenance/inspection-forms/index/details/index.js +1 -0
  882. package/app/templates/maintenance/inspection-forms/index/details/submissions.js +1 -0
  883. package/app/templates/maintenance/inspection-forms/index/details.js +1 -0
  884. package/app/templates/maintenance/inspection-forms/index/edit.js +1 -0
  885. package/app/templates/maintenance/inspection-forms/index/new.js +1 -0
  886. package/app/templates/maintenance/inspection-forms/index.js +1 -0
  887. package/app/templates/maintenance/inspection-forms.js +1 -0
  888. package/app/templates/maintenance/inspection-submissions/index/details/audit.js +1 -0
  889. package/app/templates/maintenance/inspection-submissions/index/details/index.js +1 -0
  890. package/app/templates/maintenance/inspection-submissions/index/details/photos.js +1 -0
  891. package/app/templates/maintenance/inspection-submissions/index/details.js +1 -0
  892. package/app/templates/maintenance/inspection-submissions/index/edit.js +1 -0
  893. package/app/templates/maintenance/inspection-submissions/index/new.js +1 -0
  894. package/app/templates/maintenance/inspection-submissions/index.js +1 -0
  895. package/app/templates/maintenance/inspection-submissions.js +1 -0
  896. package/app/templates/management/vehicles/index/details/inspections.js +1 -0
  897. package/app/utils/inspection-field-types.js +2 -0
  898. package/app/utils/inspection-form-structure.js +1 -0
  899. package/app/utils/radar.js +1 -0
  900. package/composer.json +1 -1
  901. package/docs/AFAQY.md +91 -0
  902. package/docs/TELEMETRY_ARCHITECTURE.md +115 -0
  903. package/extension.json +1 -1
  904. package/package.json +3 -3
  905. package/scripts/coverage-summary.php +11 -0
  906. package/scripts/pest-bootstrap.php +1 -1
  907. package/server/config/afaqy.php +8 -0
  908. package/server/config/fuel-providers.php +28 -7
  909. package/server/config/telematics.php +8 -1
  910. package/server/config/telemetry.php +17 -0
  911. package/server/migrations/2026_09_09_000001_create_inspection_tables.php +152 -0
  912. package/server/migrations/2026_09_10_000001_convert_inspection_form_items_to_field_groups.php +44 -0
  913. package/server/migrations/2026_09_10_000002_add_token_to_inspection_links.php +39 -0
  914. package/server/migrations/2026_09_11_000001_add_assignee_and_pin_to_inspection_links.php +39 -0
  915. package/server/migrations/2026_09_15_000001_create_telematic_telemetry_tables.php +62 -0
  916. package/server/migrations/2026_09_15_000002_scope_fuel_transactions_to_connection.php +25 -0
  917. package/server/migrations/2026_09_16_000001_add_public_id_to_alerts_table.php +45 -0
  918. package/server/resources/views/mail/inspection-link-pin.blade.php +34 -0
  919. package/server/src/Auth/Schemas/FleetOps.php +12 -0
  920. package/server/src/Console/Commands/DrainTelematicInbox.php +82 -0
  921. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +4 -1
  922. package/server/src/Console/Commands/SyncTelematics.php +32 -2
  923. package/server/src/Contracts/TelemetryProviderInterface.php +27 -0
  924. package/server/src/Events/DeviceTelemetryUpdated.php +37 -0
  925. package/server/src/Events/VehicleLocationChanged.php +8 -4
  926. package/server/src/Exports/InspectionExport.php +125 -0
  927. package/server/src/Http/Controllers/Api/v1/GeofenceController.php +1 -1
  928. package/server/src/Http/Controllers/Api/v1/InspectionController.php +300 -0
  929. package/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +40 -8
  930. package/server/src/Http/Controllers/Internal/v1/HubController.php +36 -11
  931. package/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +326 -0
  932. package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +229 -0
  933. package/server/src/Http/Controllers/Internal/v1/RadarController.php +1117 -0
  934. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +67 -0
  935. package/server/src/Http/Controllers/Public/PublicInspectionController.php +243 -0
  936. package/server/src/Http/Controllers/TelematicPositionWebhookController.php +60 -0
  937. package/server/src/Http/Controllers/TelematicWebhookController.php +3 -0
  938. package/server/src/Http/Filter/FuelProviderConnectionFilter.php +1 -1
  939. package/server/src/Http/Middleware/ForceJsonResponse.php +25 -0
  940. package/server/src/Http/Resources/v1/InspectionForm.php +153 -0
  941. package/server/src/Http/Resources/v1/InspectionItemResult.php +77 -0
  942. package/server/src/Http/Resources/v1/InspectionLink.php +84 -0
  943. package/server/src/Http/Resources/v1/InspectionSubmission.php +173 -0
  944. package/server/src/Jobs/PollTelematicTelemetry.php +118 -0
  945. package/server/src/Jobs/ProcessTelematicDelivery.php +122 -0
  946. package/server/src/Jobs/SyncFuelProviderTransactionsJob.php +21 -4
  947. package/server/src/Mail/InspectionLinkPinMail.php +57 -0
  948. package/server/src/Models/Driver.php +8 -2
  949. package/server/src/Models/FuelProviderTransaction.php +1 -0
  950. package/server/src/Models/InspectionForm.php +240 -0
  951. package/server/src/Models/InspectionItemResult.php +90 -0
  952. package/server/src/Models/InspectionLink.php +284 -0
  953. package/server/src/Models/InspectionSubmission.php +440 -0
  954. package/server/src/Models/Part.php +9 -2
  955. package/server/src/Models/WorkOrder.php +3 -0
  956. package/server/src/Observers/WorkOrderObserver.php +1 -1
  957. package/server/src/Providers/FleetOpsServiceProvider.php +9 -0
  958. package/server/src/Rules/Base64OrUrl.php +54 -0
  959. package/server/src/Support/FuelProviders/FuelProviderService.php +64 -27
  960. package/server/src/Support/FuelProviders/Providers/PetroAppFuelProvider.php +81 -15
  961. package/server/src/Support/InspectionFileStore.php +268 -0
  962. package/server/src/Support/InspectionFormSync.php +231 -0
  963. package/server/src/Support/InspectionLinkPin.php +150 -0
  964. package/server/src/Support/InspectionSubmitter.php +297 -0
  965. package/server/src/Support/Radar/RadarAgenda.php +375 -0
  966. package/server/src/Support/Radar/RadarBriefing.php +504 -0
  967. package/server/src/Support/Radar/RadarItemState.php +435 -0
  968. package/server/src/Support/Radar/RadarRules.php +1424 -0
  969. package/server/src/Support/Reporting/FleetOpsReportSchema.php +178 -0
  970. package/server/src/Support/Telematics/Afaqy/Payload.php +49 -0
  971. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +179 -61
  972. package/server/src/Support/Telematics/TelematicService.php +37 -6
  973. package/server/src/Support/Telematics/Telemetry/Configuration.php +30 -0
  974. package/server/src/Support/Telematics/Telemetry/Inbox.php +61 -0
  975. package/server/src/Support/Telematics/Telemetry/Ingestor.php +112 -0
  976. package/server/src/Support/Telematics/Telemetry/Queue.php +28 -0
  977. package/server/src/Support/Telematics/Telemetry/Sample.php +54 -0
  978. package/server/src/routes.php +76 -0
  979. package/server/tests/ApiManifestControllerContractsTest.php +9 -9
  980. package/server/tests/ControllerFilterContractsTest.php +1 -1
  981. package/server/tests/EventContractsTest.php +2 -1
  982. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +792 -0
  983. package/server/tests/Feature/Http/Api/GeofenceControllerContractsTest.php +4 -4
  984. package/server/tests/Feature/Http/Api/TrailerControllerLifecycleTest.php +1 -1
  985. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +1 -1
  986. package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +1 -1
  987. package/server/tests/Feature/Http/Internal/HubControllerTest.php +28 -2
  988. package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +1 -1
  989. package/server/tests/Feature/Http/Internal/RadarControllerDatabaseTest.php +415 -0
  990. package/server/tests/Feature/Http/Internal/RadarControllerTest.php +594 -0
  991. package/server/tests/FuelProviderConnectionControllerContractsTest.php +83 -3
  992. package/server/tests/FuelProviderImplementationTest.php +10 -2
  993. package/server/tests/HubAndGettingStartedContractsTest.php +26 -3
  994. package/server/tests/InspectionControllerContractsTest.php +1329 -0
  995. package/server/tests/InspectionFieldContractsTest.php +1019 -0
  996. package/server/tests/InspectionModelContractsTest.php +728 -0
  997. package/server/tests/InspectionSubmitterRulesTest.php +47 -0
  998. package/server/tests/ObserverContractsTest.php +1 -1
  999. package/server/tests/QueueJobContractsTest.php +3 -4
  1000. package/server/tests/RadarRoutesTest.php +35 -0
  1001. package/server/tests/ReportSchemaContractsTest.php +15 -1
  1002. package/server/tests/RouteRegistrationExecutionTest.php +109 -18
  1003. package/server/tests/Support/AfaqyTestCrypto.php +27 -0
  1004. package/server/tests/Support/ExampleTelemetryProvider.php +82 -0
  1005. package/server/tests/Support/TelemetryTestEnvironment.php +53 -0
  1006. package/server/tests/TelematicsHardeningTest.php +17 -17
  1007. package/server/tests/TrailerSupportCoverageTest.php +1 -1
  1008. package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +3 -3
  1009. package/server/tests/Unit/Support/FuelProviderServiceMatchingTest.php +127 -0
  1010. package/server/tests/Unit/Support/PetroAppFuelProviderTest.php +126 -1
  1011. package/server/tests/Unit/Support/RadarAgendaTest.php +200 -0
  1012. package/server/tests/Unit/Support/RadarBriefingTest.php +214 -0
  1013. package/server/tests/Unit/Support/RadarItemStateTest.php +260 -0
  1014. package/server/tests/Unit/Support/RadarRulesTest.php +523 -0
  1015. package/server/tests/Unit/Support/Telematics/Providers/AfaqyProviderTest.php +9 -1
  1016. package/server/tests/Unit/Support/Telematics/Providers/AfaqyProviderTransportTest.php +8 -0
  1017. package/server/tests/Unit/Support/Telematics/Providers/AfaqyRealtimeContractTest.php +188 -0
  1018. package/server/tests/Unit/Support/Telematics/TelematicServiceTelemetryTest.php +1 -0
  1019. package/translations/ar-ae.yml +3 -1
  1020. package/translations/bg-bg.yaml +3 -1
  1021. package/translations/en-us.yaml +738 -15
  1022. package/translations/es-pa.yaml +3 -1
  1023. package/translations/fr-fr.yaml +3 -1
  1024. package/translations/mn-mn.yaml +3 -1
  1025. package/translations/pt-br.yaml +3 -1
  1026. package/translations/ru-ru.yaml +3 -1
  1027. package/translations/vi-vn.yaml +3 -1
  1028. package/addon/components/cell/attached-vehicle.hbs +0 -10
  1029. package/addon/components/cell/attached-vehicle.js +0 -58
  1030. package/addon/components/cell/device-identity.js +0 -129
  1031. package/addon/components/cell/driver-identity.js +0 -143
  1032. package/addon/components/cell/driver-name.hbs +0 -26
  1033. package/addon/components/cell/driver-name.js +0 -30
  1034. package/addon/components/cell/equipment-identity.js +0 -48
  1035. package/addon/components/cell/part-identity.js +0 -63
  1036. package/addon/components/cell/telematic-device.hbs +0 -38
  1037. package/addon/components/cell/telematic-device.js +0 -44
  1038. package/addon/components/cell/telematic-provider.hbs +0 -33
  1039. package/addon/components/cell/telematic-provider.js +0 -78
  1040. package/addon/components/cell/vehicle-identity.js +0 -138
  1041. package/addon/components/cell/vehicle-name.hbs +0 -23
  1042. package/addon/components/cell/vehicle-name.js +0 -3
  1043. package/addon/components/device/pill.js +0 -3
  1044. package/addon/components/driver/pill.js +0 -7
  1045. package/addon/components/equipment/pill.js +0 -3
  1046. package/addon/components/filter/multi-model.hbs +0 -19
  1047. package/addon/components/filter/multi-model.js +0 -90
  1048. package/addon/components/order/pill.js +0 -3
  1049. package/addon/components/trailer/pill.js +0 -3
  1050. package/addon/components/vehicle/pill.js +0 -7
  1051. package/addon/templates/connectivity/fuel-providers/details/settings.hbs +0 -11
@@ -0,0 +1,1424 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Radar;
4
+
5
+ use Illuminate\Support\Carbon;
6
+ use Illuminate\Support\Str;
7
+
8
+ /**
9
+ * Turns plain rows from the resource tables into Radar items: the per-record
10
+ * gaps a fleet manager has to decide about.
11
+ *
12
+ * Everything here is pure. The controller loads the rows and hands them over
13
+ * as arrays; this class never queries, never reads the session, and takes the
14
+ * clock as an argument, so a test can feed it a fixture and a fixed "now".
15
+ */
16
+ class RadarRules
17
+ {
18
+ public const CATEGORY_MAINTENANCE = 'maintenance';
19
+ public const CATEGORY_INSPECTIONS = 'inspections';
20
+ public const CATEGORY_STAFFING = 'staffing';
21
+ public const CATEGORY_COMPLIANCE = 'compliance';
22
+ public const CATEGORY_FUEL = 'fuel';
23
+ public const CATEGORY_PARTS = 'parts';
24
+ public const CATEGORY_CONNECTIVITY = 'connectivity';
25
+ public const CATEGORY_ISSUES = 'issues';
26
+ public const CATEGORY_NOTICES = 'notices';
27
+
28
+ public const LANE_SHIFTS = 'shifts';
29
+ public const LANE_MAINTENANCE = 'maintenance';
30
+ public const LANE_EXPIRIES = 'expiries';
31
+ public const LANE_NOTICES = 'notices';
32
+ public const LANE_ANYTIME = 'anytime';
33
+
34
+ public const SEVERITY_CRITICAL = 'critical';
35
+ public const SEVERITY_WARNING = 'warning';
36
+ public const SEVERITY_INFO = 'info';
37
+
38
+ public const NOTICE_TYPE = 'radar_notice';
39
+
40
+ /** How far ahead "due soon" and "expiring" look. */
41
+ public const DUE_SOON_DAYS = 7;
42
+ public const EXPIRING_DAYS = 30;
43
+
44
+ /** A draft inspection older than this is one nobody is going to finish. */
45
+ public const STALE_DRAFT_HOURS = 24;
46
+
47
+ /** An inspection link expiring within this window is worth a nudge. */
48
+ public const LINK_EXPIRY_HOURS = 24;
49
+
50
+ /** How late a scheduled shift start is before it becomes an item, and when it turns critical. */
51
+ public const LATE_START_MINUTES = 15;
52
+ public const LATE_START_CRITICAL = 60;
53
+
54
+ /** A shift ending inside this window with orders still on the driver needs a handover. */
55
+ public const HANDOVER_MINUTES = 60;
56
+
57
+ /** The default low-stock threshold when neither the column nor the spec says otherwise. */
58
+ public const DEFAULT_LOW_STOCK = 5;
59
+
60
+ /**
61
+ * Every rule, with the category it reports under, the agenda lane it lands
62
+ * on when dated, and the chip label the console shows.
63
+ */
64
+ public const RULES = [
65
+ 'maintenance_overdue' => ['category' => self::CATEGORY_MAINTENANCE, 'lane' => self::LANE_MAINTENANCE, 'chip' => 'Maint'],
66
+ 'maintenance_due_soon' => ['category' => self::CATEGORY_MAINTENANCE, 'lane' => self::LANE_MAINTENANCE, 'chip' => 'Maint'],
67
+ 'inspection_due' => ['category' => self::CATEGORY_INSPECTIONS, 'lane' => self::LANE_MAINTENANCE, 'chip' => 'Inspection'],
68
+ 'inspection_failed' => ['category' => self::CATEGORY_INSPECTIONS, 'lane' => self::LANE_ANYTIME, 'chip' => 'Inspection'],
69
+ 'inspection_unresolved' => ['category' => self::CATEGORY_INSPECTIONS, 'lane' => self::LANE_ANYTIME, 'chip' => 'Inspection'],
70
+ 'inspection_draft' => ['category' => self::CATEGORY_INSPECTIONS, 'lane' => self::LANE_ANYTIME, 'chip' => 'Inspection'],
71
+ 'inspection_link_pending' => ['category' => self::CATEGORY_INSPECTIONS, 'lane' => self::LANE_EXPIRIES, 'chip' => 'Inspection'],
72
+ 'vehicle_inspection_failed' => ['category' => self::CATEGORY_INSPECTIONS, 'lane' => self::LANE_ANYTIME, 'chip' => 'Inspection'],
73
+ 'work_order_overdue' => ['category' => self::CATEGORY_MAINTENANCE, 'lane' => self::LANE_MAINTENANCE, 'chip' => 'Work order'],
74
+ 'work_order_blocked' => ['category' => self::CATEGORY_MAINTENANCE, 'lane' => self::LANE_MAINTENANCE, 'chip' => 'Work order'],
75
+ 'issue_open' => ['category' => self::CATEGORY_ISSUES, 'lane' => self::LANE_ANYTIME, 'chip' => 'Issue'],
76
+ 'shift_late_start' => ['category' => self::CATEGORY_STAFFING, 'lane' => self::LANE_SHIFTS, 'chip' => 'Shift'],
77
+ 'shift_no_vehicle' => ['category' => self::CATEGORY_STAFFING, 'lane' => self::LANE_SHIFTS, 'chip' => 'Shift'],
78
+ 'shift_handover' => ['category' => self::CATEGORY_STAFFING, 'lane' => self::LANE_SHIFTS, 'chip' => 'Handover'],
79
+ 'driver_without_vehicle' => ['category' => self::CATEGORY_STAFFING, 'lane' => self::LANE_ANYTIME, 'chip' => 'Unassigned'],
80
+ 'vehicle_without_driver' => ['category' => self::CATEGORY_STAFFING, 'lane' => self::LANE_ANYTIME, 'chip' => 'Idle'],
81
+ 'vehicle_without_device' => ['category' => self::CATEGORY_CONNECTIVITY, 'lane' => self::LANE_ANYTIME, 'chip' => 'Device'],
82
+ 'device_unattached' => ['category' => self::CATEGORY_CONNECTIVITY, 'lane' => self::LANE_ANYTIME, 'chip' => 'Device'],
83
+ 'license_expiring' => ['category' => self::CATEGORY_COMPLIANCE, 'lane' => self::LANE_EXPIRIES, 'chip' => 'Licence'],
84
+ 'lease_expiring' => ['category' => self::CATEGORY_COMPLIANCE, 'lane' => self::LANE_EXPIRIES, 'chip' => 'Lease'],
85
+ 'fuel_unmatched' => ['category' => self::CATEGORY_FUEL, 'lane' => self::LANE_ANYTIME, 'chip' => 'Fuel'],
86
+ 'part_low_stock' => ['category' => self::CATEGORY_PARTS, 'lane' => self::LANE_ANYTIME, 'chip' => 'Parts'],
87
+ 'notice' => ['category' => self::CATEGORY_NOTICES, 'lane' => self::LANE_NOTICES, 'chip' => 'Notice'],
88
+ ];
89
+
90
+ /**
91
+ * The count pills, each the set of rules (or the due bucket) it counts.
92
+ */
93
+ public const PILLS = [
94
+ 'overdue' => ['buckets' => ['overdue']],
95
+ 'due_week' => ['buckets' => ['today', 'week']],
96
+ 'unassigned' => ['rules' => ['driver_without_vehicle', 'shift_no_vehicle', 'vehicle_without_driver']],
97
+ 'issues' => ['rules' => ['issue_open']],
98
+ 'inspections' => ['rules' => ['inspection_due', 'inspection_failed', 'inspection_unresolved', 'inspection_draft', 'inspection_link_pending', 'vehicle_inspection_failed']],
99
+ 'shifts' => ['rules' => ['shift_late_start', 'shift_no_vehicle', 'shift_handover']],
100
+ 'expiring' => ['rules' => ['license_expiring', 'lease_expiring', 'inspection_link_pending']],
101
+ 'low_stock' => ['rules' => ['part_low_stock']],
102
+ 'fuel' => ['rules' => ['fuel_unmatched']],
103
+ 'notices' => ['rules' => ['notice']],
104
+ ];
105
+
106
+ /** Work order statuses that mean the work is done with. */
107
+ public const WORK_ORDER_CLOSED = ['closed', 'completed', 'done', 'canceled', 'cancelled'];
108
+
109
+ /** Work order statuses that mean the work is waiting on something. */
110
+ public const WORK_ORDER_BLOCKED = ['blocked', 'awaiting_parts', 'awaiting_vendor', 'on_hold'];
111
+
112
+ /** Issue priorities that make an open issue critical. */
113
+ public const ISSUE_CRITICAL_PRIORITIES = ['high', 'critical', 'urgent'];
114
+
115
+ /** Shift statuses that count as a shift the driver is expected to work. */
116
+ public const SHIFT_LIVE_STATUSES = ['pending', 'scheduled', 'confirmed', 'in_progress'];
117
+
118
+ /** Shift statuses in which a driver should have shown up by the start time. */
119
+ public const SHIFT_NOT_STARTED_STATUSES = ['pending', 'scheduled', 'confirmed'];
120
+
121
+ /**
122
+ * Build every item from the given source rows.
123
+ *
124
+ * @param array $sources keyed by source name; each a list of plain arrays (see the load* methods on RadarController)
125
+ *
126
+ * @return array{items: array, counts: array, summary: array}
127
+ */
128
+ public static function build(array $sources, Carbon $now): array
129
+ {
130
+ $items = [];
131
+
132
+ foreach (self::scheduleItems($sources['schedules'] ?? [], $now) as $item) {
133
+ $items[] = $item;
134
+ }
135
+ foreach (self::workOrderItems($sources['workOrders'] ?? [], $now) as $item) {
136
+ $items[] = $item;
137
+ }
138
+ foreach (self::issueItems($sources['issues'] ?? [], $now) as $item) {
139
+ $items[] = $item;
140
+ }
141
+ foreach (self::inspectionItems($sources['inspectionSubmissions'] ?? [], $now) as $item) {
142
+ $items[] = $item;
143
+ }
144
+ foreach (self::inspectionLinkItems($sources['inspectionLinks'] ?? [], $now) as $item) {
145
+ $items[] = $item;
146
+ }
147
+ foreach (self::shiftItems($sources['shifts'] ?? [], $now) as $item) {
148
+ $items[] = $item;
149
+ }
150
+ foreach (self::driverItems($sources['drivers'] ?? [], $sources['shifts'] ?? [], $now) as $item) {
151
+ $items[] = $item;
152
+ }
153
+ foreach (self::vehicleItems($sources['vehicles'] ?? [], $sources['devices'] ?? [], $now) as $item) {
154
+ $items[] = $item;
155
+ }
156
+ foreach (self::trailerItems($sources['trailers'] ?? [], $now) as $item) {
157
+ $items[] = $item;
158
+ }
159
+ foreach (self::deviceItems($sources['devices'] ?? [], $now) as $item) {
160
+ $items[] = $item;
161
+ }
162
+ foreach (self::partItems($sources['parts'] ?? [], $sources['workOrders'] ?? [], $now) as $item) {
163
+ $items[] = $item;
164
+ }
165
+ foreach (self::fuelItems($sources['fuelTransactions'] ?? [], $now) as $item) {
166
+ $items[] = $item;
167
+ }
168
+ foreach (self::noticeItems($sources['notices'] ?? [], $now) as $item) {
169
+ $items[] = $item;
170
+ }
171
+
172
+ $items = self::mergeStates($items, $sources['states'] ?? [], $now);
173
+ $items = self::sort($items);
174
+
175
+ $open = array_values(array_filter($items, fn ($item) => self::isOpen($item, $now)));
176
+
177
+ return [
178
+ 'items' => $items,
179
+ 'counts' => self::counts($open),
180
+ 'summary' => self::summary($items, $now),
181
+ ];
182
+ }
183
+
184
+ // ------------------------------------------------------------------
185
+ // Rules
186
+ // ------------------------------------------------------------------
187
+
188
+ /**
189
+ * Maintenance schedules: overdue, due within a week, and inspection-type
190
+ * schedules as their own rule so the chip says what the job is.
191
+ */
192
+ public static function scheduleItems(array $schedules, Carbon $now): array
193
+ {
194
+ $items = [];
195
+
196
+ foreach ($schedules as $schedule) {
197
+ $due = self::carbon($schedule['next_due_date'] ?? null);
198
+ if (!$due || ($schedule['status'] ?? 'active') !== 'active') {
199
+ continue;
200
+ }
201
+
202
+ $isInspection = ($schedule['type'] ?? null) === 'inspection';
203
+ $isOverdue = $due->lt($now);
204
+ $isDueSoon = !$isOverdue && $due->lte($now->copy()->addDays(self::DUE_SOON_DAYS));
205
+
206
+ if (!$isOverdue && !$isDueSoon) {
207
+ continue;
208
+ }
209
+
210
+ $rule = $isInspection ? 'inspection_due' : ($isOverdue ? 'maintenance_overdue' : 'maintenance_due_soon');
211
+ $name = $schedule['name'] ?: ($isInspection ? 'Inspection' : 'Service');
212
+
213
+ if ($isOverdue) {
214
+ $title = sprintf('%s %s', $name, self::overdueWords($due, $now));
215
+ } else {
216
+ $title = sprintf('%s due %s', $name, self::dueInWords($due, $now));
217
+ }
218
+
219
+ $meta = [];
220
+ if (!empty($schedule['next_due_odometer'])) {
221
+ $meta[] = 'due at ' . number_format((int) $schedule['next_due_odometer']) . ' odometer';
222
+ }
223
+ if (!empty($schedule['has_open_work_order'])) {
224
+ $meta[] = 'work order open';
225
+ }
226
+
227
+ $items[] = self::item($rule, $schedule['subject'] ?? null, $title, [
228
+ 'severity' => $isOverdue ? self::SEVERITY_CRITICAL : self::SEVERITY_WARNING,
229
+ 'due_at' => $due,
230
+ 'meta_line' => implode(' · ', $meta) ?: null,
231
+ 'record' => self::record('maintenance.schedules.index.details', $schedule['public_id'] ?? null),
232
+ 'source' => ['type' => 'schedule', 'uuid' => $schedule['uuid'] ?? null, 'public_id' => $schedule['public_id'] ?? null, 'has_open_work_order' => (bool) ($schedule['has_open_work_order'] ?? false)],
233
+ 'actions' => array_keys(array_filter(['create_work_order' => empty($schedule['has_open_work_order']), 'acknowledge' => true, 'snooze' => true, 'assign' => true, 'open_record' => true])),
234
+ ], $now, $schedule['public_id'] ?? ($schedule['uuid'] ?? ''));
235
+ }
236
+
237
+ return $items;
238
+ }
239
+
240
+ /**
241
+ * Work orders past due, or waiting on parts, a vendor, or a hold.
242
+ */
243
+ public static function workOrderItems(array $workOrders, Carbon $now): array
244
+ {
245
+ $items = [];
246
+
247
+ foreach ($workOrders as $workOrder) {
248
+ $status = $workOrder['status'] ?? 'open';
249
+ if (in_array($status, self::WORK_ORDER_CLOSED, true)) {
250
+ continue;
251
+ }
252
+
253
+ $due = self::carbon($workOrder['due_at'] ?? null);
254
+ $isOverdue = $due && $due->lt($now);
255
+ $isBlocked = in_array($status, self::WORK_ORDER_BLOCKED, true);
256
+
257
+ if (!$isOverdue && !$isBlocked) {
258
+ continue;
259
+ }
260
+
261
+ $code = $workOrder['code'] ?? $workOrder['public_id'] ?? 'Work order';
262
+ $title = trim($code . ' ' . ($workOrder['subject'] ?? ''));
263
+ $meta = [];
264
+
265
+ if ($isBlocked) {
266
+ $meta[] = Str::of($status)->replace('_', ' ')->toString();
267
+ }
268
+ if (!empty($workOrder['priority']) && $workOrder['priority'] !== 'normal') {
269
+ $meta[] = $workOrder['priority'] . ' priority';
270
+ }
271
+
272
+ $items[] = self::item($isOverdue ? 'work_order_overdue' : 'work_order_blocked', $workOrder['target'] ?? null, $title, [
273
+ 'severity' => $isOverdue ? self::SEVERITY_CRITICAL : self::SEVERITY_WARNING,
274
+ 'due_at' => $due,
275
+ 'meta_line' => implode(' · ', $meta) ?: null,
276
+ 'record' => self::record('maintenance.work-orders.index.details', $workOrder['public_id'] ?? null),
277
+ 'source' => ['type' => 'work_order', 'uuid' => $workOrder['uuid'] ?? null, 'public_id' => $workOrder['public_id'] ?? null, 'status' => $status],
278
+ 'actions' => ['acknowledge', 'snooze', 'assign', 'open_record'],
279
+ ], $now, $workOrder['public_id'] ?? ($workOrder['uuid'] ?? ''));
280
+ }
281
+
282
+ return $items;
283
+ }
284
+
285
+ /**
286
+ * Open issues, critical when their priority says so. An issue an
287
+ * inspection raised says so in its meta line.
288
+ */
289
+ public static function issueItems(array $issues, Carbon $now): array
290
+ {
291
+ $items = [];
292
+
293
+ foreach ($issues as $issue) {
294
+ if (in_array($issue['status'] ?? 'pending', ['resolved', 'closed'], true)) {
295
+ continue;
296
+ }
297
+
298
+ $priority = strtolower((string) ($issue['priority'] ?? ''));
299
+ $subject = $issue['vehicle'] ?? $issue['driver'] ?? null;
300
+ $title = $issue['title'] ?: Str::limit((string) ($issue['report'] ?? 'Open issue'), 80);
301
+ $meta = [];
302
+
303
+ if ($priority) {
304
+ $meta[] = $priority . ' priority';
305
+ }
306
+ if (!empty($issue['meta']['inspection_submission_id'])) {
307
+ $meta[] = 'raised by inspection ' . $issue['meta']['inspection_submission_id'];
308
+ } elseif (!empty($issue['meta']['inspection_submission_uuid'])) {
309
+ $meta[] = 'raised by an inspection';
310
+ }
311
+ if (!empty($issue['reporter_name'])) {
312
+ $meta[] = 'reported by ' . $issue['reporter_name'];
313
+ }
314
+
315
+ $items[] = self::item('issue_open', $subject, $title, [
316
+ 'severity' => in_array($priority, self::ISSUE_CRITICAL_PRIORITIES, true) ? self::SEVERITY_CRITICAL : self::SEVERITY_WARNING,
317
+ 'meta_line' => implode(' · ', $meta) ?: null,
318
+ 'record' => self::record('management.issues.index.details', $issue['public_id'] ?? null),
319
+ 'source' => ['type' => 'issue', 'uuid' => $issue['uuid'] ?? null, 'public_id' => $issue['public_id'] ?? null, 'status' => $issue['status'] ?? null],
320
+ 'actions' => ['resolve_issue', 'acknowledge', 'snooze', 'assign', 'open_record'],
321
+ ], $now, $issue['public_id'] ?? ($issue['uuid'] ?? ''));
322
+ }
323
+
324
+ return $items;
325
+ }
326
+
327
+ /**
328
+ * Inspection submissions: failed ones with follow-up still to raise,
329
+ * failed ones waiting on their follow-up, and drafts nobody finished.
330
+ */
331
+ public static function inspectionItems(array $submissions, Carbon $now): array
332
+ {
333
+ $items = [];
334
+
335
+ foreach ($submissions as $submission) {
336
+ $status = $submission['status'] ?? 'draft';
337
+ $result = $submission['result'] ?? null;
338
+ $failedItems = (int) ($submission['failed_items'] ?? 0);
339
+ $hasFailures = $failedItems > 0 || $result === 'failed';
340
+ $subject = $submission['vehicle'] ?? $submission['driver'] ?? null;
341
+ $formName = $submission['form_name'] ?? 'Inspection';
342
+ $publicId = $submission['public_id'] ?? ($submission['uuid'] ?? '');
343
+ $record = self::record('maintenance.inspection-submissions.index.details', $submission['public_id'] ?? null);
344
+ $source = ['type' => 'inspection_submission', 'uuid' => $submission['uuid'] ?? null, 'public_id' => $submission['public_id'] ?? null, 'status' => $status, 'result' => $result, 'issue_uuid' => $submission['issue_uuid'] ?? null, 'work_order_uuid' => $submission['work_order_uuid'] ?? null];
345
+
346
+ if ($status === 'draft') {
347
+ $started = self::carbon($submission['started_at'] ?? $submission['created_at'] ?? null);
348
+ if (!$started || $started->gt($now->copy()->subHours(self::STALE_DRAFT_HOURS))) {
349
+ continue;
350
+ }
351
+
352
+ $items[] = self::item('inspection_draft', $subject, sprintf('%s started %s, never filed', $formName, self::agoWords($started, $now)), [
353
+ 'severity' => self::SEVERITY_INFO,
354
+ 'meta_line' => !empty($submission['driver_name']) ? 'by ' . $submission['driver_name'] : null,
355
+ 'record' => $record,
356
+ 'source' => $source,
357
+ 'actions' => ['open_record', 'acknowledge', 'snooze', 'assign'],
358
+ ], $now, $publicId);
359
+ continue;
360
+ }
361
+
362
+ if ($status === 'resolved' || !$hasFailures) {
363
+ continue;
364
+ }
365
+
366
+ $needsIssue = empty($submission['issue_uuid']);
367
+ $needsWorkOrder = empty($submission['work_order_uuid']);
368
+ $severity = strtolower((string) ($submission['highest_severity'] ?? 'high'));
369
+ $criticalWords = $severity === 'critical' ? 'critical ' : '';
370
+
371
+ if ($needsIssue || $needsWorkOrder) {
372
+ $missing = [];
373
+ if ($needsIssue) {
374
+ $missing[] = 'no issue';
375
+ }
376
+ if ($needsWorkOrder) {
377
+ $missing[] = 'no work order';
378
+ }
379
+
380
+ $actions = [];
381
+ if ($needsWorkOrder) {
382
+ $actions[] = 'create_work_order_from_inspection';
383
+ }
384
+ if ($needsIssue) {
385
+ $actions[] = 'create_issue_from_inspection';
386
+ }
387
+
388
+ $items[] = self::item('inspection_failed', $subject, sprintf('%s failed %d %sitem%s', $formName, $failedItems, $criticalWords, $failedItems === 1 ? '' : 's'), [
389
+ 'severity' => $severity === 'critical' ? self::SEVERITY_CRITICAL : self::SEVERITY_WARNING,
390
+ 'meta_line' => implode(' · ', array_merge([$failedItems . ' failed'], $missing)),
391
+ 'record' => $record,
392
+ 'source' => $source,
393
+ 'actions' => array_merge($actions, ['acknowledge', 'snooze', 'assign', 'open_record']),
394
+ 'details' => ['failed_items' => $submission['failed_item_labels'] ?? []],
395
+ ], $now, $publicId);
396
+ continue;
397
+ }
398
+
399
+ if (empty($submission['resolved_at'])) {
400
+ $items[] = self::item('inspection_unresolved', $subject, sprintf('%s follow-up still open', $formName), [
401
+ 'severity' => self::SEVERITY_INFO,
402
+ 'meta_line' => $failedItems . ' failed · issue and work order raised',
403
+ 'record' => $record,
404
+ 'source' => $source,
405
+ 'actions' => ['resolve_inspection', 'acknowledge', 'snooze', 'assign', 'open_record'],
406
+ ], $now, $publicId);
407
+ }
408
+ }
409
+
410
+ return $items;
411
+ }
412
+
413
+ /**
414
+ * Inspection links that were sent but never used, once they are close to
415
+ * expiring or already have.
416
+ */
417
+ public static function inspectionLinkItems(array $links, Carbon $now): array
418
+ {
419
+ $items = [];
420
+
421
+ foreach ($links as $link) {
422
+ if (($link['status'] ?? 'active') !== 'active' || !empty($link['used_at'])) {
423
+ continue;
424
+ }
425
+
426
+ $expires = self::carbon($link['expires_at'] ?? null);
427
+ if (!$expires || $expires->gt($now->copy()->addHours(self::LINK_EXPIRY_HOURS))) {
428
+ continue;
429
+ }
430
+
431
+ $expired = $expires->lte($now);
432
+ $subject = $link['driver'] ?? $link['vehicle'] ?? null;
433
+ $formName = $link['form_name'] ?? 'Inspection';
434
+ $title = $expired
435
+ ? sprintf('%s link expired unused', $formName)
436
+ : sprintf('%s link expires %s, not yet used', $formName, self::dueInWords($expires, $now));
437
+
438
+ $items[] = self::item('inspection_link_pending', $subject, $title, [
439
+ 'severity' => $expired ? self::SEVERITY_WARNING : self::SEVERITY_INFO,
440
+ 'due_at' => $expires,
441
+ 'meta_line' => !empty($link['last_viewed_at']) ? 'opened, not filed' : 'never opened',
442
+ 'record' => self::record('maintenance.inspection-forms.index.details', $link['form_public_id'] ?? null),
443
+ 'source' => ['type' => 'inspection_link', 'uuid' => $link['uuid'] ?? null, 'public_id' => $link['public_id'] ?? null, 'form_uuid' => $link['form_uuid'] ?? null, 'form_public_id' => $link['form_public_id'] ?? null, 'state' => $expired ? 'expired' : 'active'],
444
+ 'actions' => ['send_pin', 'revoke_link', 'acknowledge', 'snooze', 'assign', 'open_record'],
445
+ ], $now, $link['public_id'] ?? ($link['uuid'] ?? ''));
446
+ }
447
+
448
+ return $items;
449
+ }
450
+
451
+ /**
452
+ * Shifts happening now: a driver who should have started but is not
453
+ * online, a driver on shift with no vehicle, and a shift ending soon with
454
+ * orders still on the driver.
455
+ */
456
+ public static function shiftItems(array $shifts, Carbon $now): array
457
+ {
458
+ $items = [];
459
+
460
+ foreach ($shifts as $shift) {
461
+ $start = self::carbon($shift['start_at'] ?? null);
462
+ $end = self::carbon($shift['end_at'] ?? null);
463
+ $status = $shift['status'] ?? 'scheduled';
464
+ $driver = $shift['driver'] ?? null;
465
+
466
+ if (!$start || !$end || !$driver || !in_array($status, self::SHIFT_LIVE_STATUSES, true)) {
467
+ continue;
468
+ }
469
+
470
+ if ($end->lte($now) || $start->gt($now)) {
471
+ continue;
472
+ }
473
+
474
+ $window = ['start_at' => $start->toIso8601String(), 'end_at' => $end->toIso8601String(), 'status' => $status];
475
+ $name = $driver['label'] ?? 'Driver';
476
+ $publicId = $driver['public_id'] ?? ($driver['uuid'] ?? '');
477
+ $source = ['type' => 'shift', 'uuid' => $shift['uuid'] ?? null, 'public_id' => $shift['public_id'] ?? null, 'driver_uuid' => $driver['uuid'] ?? null];
478
+ $record = self::record('management.drivers.index.details', $driver['public_id'] ?? null);
479
+
480
+ $minutesLate = $start->diffInMinutes($now, false);
481
+ if (empty($shift['driver_online']) && in_array($status, self::SHIFT_NOT_STARTED_STATUSES, true) && $minutesLate >= self::LATE_START_MINUTES) {
482
+ $items[] = self::item('shift_late_start', $driver, sprintf('%s scheduled %s, not online', $name, $start->format('H:i')), [
483
+ 'severity' => $minutesLate >= self::LATE_START_CRITICAL ? self::SEVERITY_CRITICAL : self::SEVERITY_WARNING,
484
+ 'meta_line' => self::minutesWords($minutesLate) . ' late',
485
+ 'window' => $window,
486
+ 'record' => $record,
487
+ 'source' => $source,
488
+ 'actions' => ['call', 'cover_shift', 'acknowledge', 'snooze', 'assign', 'open_record'],
489
+ ], $now, $publicId);
490
+ }
491
+
492
+ if (empty($shift['driver_vehicle_uuid'])) {
493
+ $items[] = self::item('shift_no_vehicle', $driver, sprintf('%s on shift since %s, no vehicle assigned', $name, $start->format('H:i')), [
494
+ 'severity' => self::SEVERITY_WARNING,
495
+ 'meta_line' => self::minutesWords($start->diffInMinutes($now)) . ' on shift',
496
+ 'window' => $window,
497
+ 'record' => $record,
498
+ 'source' => $source,
499
+ 'actions' => ['assign_vehicle', 'acknowledge', 'snooze', 'assign', 'open_record'],
500
+ ], $now, $publicId);
501
+ }
502
+
503
+ $activeOrders = (int) ($shift['active_orders'] ?? 0);
504
+ $minutesToEnd = $now->diffInMinutes($end, false);
505
+ if ($activeOrders > 0 && $minutesToEnd <= self::HANDOVER_MINUTES) {
506
+ $items[] = self::item('shift_handover', $driver, sprintf('%s shift ends in %s, %d active order%s still assigned', $name, self::minutesWords($minutesToEnd), $activeOrders, $activeOrders === 1 ? '' : 's'), [
507
+ 'severity' => self::SEVERITY_WARNING,
508
+ 'due_at' => $end,
509
+ 'window' => $window,
510
+ 'record' => $record,
511
+ 'source' => $source + ['active_orders' => $activeOrders],
512
+ 'actions' => ['handover', 'extend_shift', 'acknowledge', 'snooze', 'assign', 'open_record'],
513
+ ], $now, $publicId);
514
+ }
515
+ }
516
+
517
+ return $items;
518
+ }
519
+
520
+ /**
521
+ * Drivers with no vehicle (when not on shift — the shift rule covers
522
+ * that) and driver licences expiring.
523
+ */
524
+ public static function driverItems(array $drivers, array $shifts, Carbon $now): array
525
+ {
526
+ $items = [];
527
+ $onShift = [];
528
+
529
+ foreach ($shifts as $shift) {
530
+ $start = self::carbon($shift['start_at'] ?? null);
531
+ $end = self::carbon($shift['end_at'] ?? null);
532
+ if ($start && $end && $start->lte($now) && $end->gt($now) && in_array($shift['status'] ?? 'scheduled', self::SHIFT_LIVE_STATUSES, true)) {
533
+ $onShift[$shift['driver']['uuid'] ?? ''] = true;
534
+ }
535
+ }
536
+
537
+ foreach ($drivers as $driver) {
538
+ $subject = self::driverSubject($driver);
539
+ $publicId = $driver['public_id'] ?? ($driver['uuid'] ?? '');
540
+ $record = self::record('management.drivers.index.details', $driver['public_id'] ?? null);
541
+
542
+ if (empty($driver['vehicle_uuid']) && empty($onShift[$driver['uuid'] ?? ''])) {
543
+ $items[] = self::item('driver_without_vehicle', $subject, sprintf('%s has no vehicle assigned', $subject['label']), [
544
+ 'severity' => !empty($driver['online']) ? self::SEVERITY_WARNING : self::SEVERITY_INFO,
545
+ 'meta_line' => !empty($driver['online']) ? 'online now' : null,
546
+ 'record' => $record,
547
+ 'source' => ['type' => 'driver', 'uuid' => $driver['uuid'] ?? null, 'public_id' => $driver['public_id'] ?? null],
548
+ 'actions' => ['assign_vehicle', 'acknowledge', 'snooze', 'assign', 'open_record'],
549
+ ], $now, $publicId);
550
+ }
551
+
552
+ $expiry = self::carbon($driver['license_expiry'] ?? null);
553
+ if ($expiry && $expiry->lte($now->copy()->addDays(self::EXPIRING_DAYS))) {
554
+ $expired = $expiry->lt($now->copy()->startOfDay());
555
+ $items[] = self::item('license_expiring', $subject, $expired
556
+ ? sprintf('%s licence expired %s', $subject['label'], $expiry->format('j M'))
557
+ : sprintf('%s licence expires %s', $subject['label'], self::dueInWords($expiry, $now)), [
558
+ 'severity' => $expired ? self::SEVERITY_CRITICAL : self::SEVERITY_WARNING,
559
+ 'due_at' => $expiry,
560
+ 'meta_line' => !empty($driver['drivers_license_number']) ? 'licence ' . $driver['drivers_license_number'] : null,
561
+ 'record' => $record,
562
+ 'source' => ['type' => 'driver', 'uuid' => $driver['uuid'] ?? null, 'public_id' => $driver['public_id'] ?? null],
563
+ 'actions' => ['acknowledge', 'snooze', 'assign', 'open_record'],
564
+ ], $now, $publicId);
565
+ }
566
+ }
567
+
568
+ return $items;
569
+ }
570
+
571
+ /**
572
+ * Vehicles with no driver, no device (while spare devices exist), a
573
+ * failed-inspection status, or a lease running out.
574
+ */
575
+ public static function vehicleItems(array $vehicles, array $devices, Carbon $now): array
576
+ {
577
+ $items = [];
578
+ $spareDevices = count(array_filter($devices, fn ($device) => empty($device['attachable_uuid'])));
579
+
580
+ foreach ($vehicles as $vehicle) {
581
+ $subject = self::vehicleSubject($vehicle);
582
+ $publicId = $vehicle['public_id'] ?? ($vehicle['uuid'] ?? '');
583
+ $record = self::record('management.vehicles.index.details', $vehicle['public_id'] ?? null);
584
+ $source = ['type' => 'vehicle', 'uuid' => $vehicle['uuid'] ?? null, 'public_id' => $vehicle['public_id'] ?? null, 'status' => $vehicle['status'] ?? null];
585
+
586
+ if (($vehicle['status'] ?? null) === 'inspection_failed') {
587
+ $items[] = self::item('vehicle_inspection_failed', $subject, sprintf('%s failed inspection, cannot dispatch', $subject['label']), [
588
+ 'severity' => self::SEVERITY_CRITICAL,
589
+ 'meta_line' => 'status inspection_failed',
590
+ 'record' => self::record('management.vehicles.index.details.inspections', $vehicle['public_id'] ?? null),
591
+ 'source' => $source,
592
+ 'actions' => ['acknowledge', 'snooze', 'assign', 'open_record'],
593
+ ], $now, $publicId);
594
+ }
595
+
596
+ if (empty($vehicle['driver_uuid']) && in_array($vehicle['status'] ?? 'available', ['available', 'active', 'idle', 'operational'], true)) {
597
+ $meta = [];
598
+ if (!empty($vehicle['lease_expires_at'])) {
599
+ $lease = self::carbon($vehicle['lease_expires_at']);
600
+ if ($lease) {
601
+ $meta[] = 'lease ends ' . $lease->format('j M');
602
+ }
603
+ }
604
+
605
+ $items[] = self::item('vehicle_without_driver', $subject, sprintf('%s has no driver', $subject['label']), [
606
+ 'severity' => self::SEVERITY_INFO,
607
+ 'meta_line' => implode(' · ', $meta) ?: null,
608
+ 'record' => $record,
609
+ 'source' => $source,
610
+ 'actions' => ['assign_driver', 'acknowledge', 'snooze', 'assign', 'open_record'],
611
+ ], $now, $publicId);
612
+ }
613
+
614
+ if ((int) ($vehicle['device_count'] ?? 0) === 0 && $spareDevices > 0) {
615
+ $items[] = self::item('vehicle_without_device', $subject, sprintf('%s has no tracking device', $subject['label']), [
616
+ 'severity' => self::SEVERITY_INFO,
617
+ 'meta_line' => sprintf('%d spare device%s', $spareDevices, $spareDevices === 1 ? '' : 's'),
618
+ 'record' => self::record('management.vehicles.index.details.devices', $vehicle['public_id'] ?? null),
619
+ 'source' => $source,
620
+ 'actions' => ['acknowledge', 'snooze', 'assign', 'open_record'],
621
+ ], $now, $publicId);
622
+ }
623
+
624
+ $lease = self::carbon($vehicle['lease_expires_at'] ?? null);
625
+ if ($lease && $lease->lte($now->copy()->addDays(self::EXPIRING_DAYS))) {
626
+ $items[] = self::leaseItem($subject, $lease, $record, $source, $now, $publicId);
627
+ }
628
+ }
629
+
630
+ return $items;
631
+ }
632
+
633
+ /**
634
+ * Trailers whose lease is running out.
635
+ */
636
+ public static function trailerItems(array $trailers, Carbon $now): array
637
+ {
638
+ $items = [];
639
+
640
+ foreach ($trailers as $trailer) {
641
+ $lease = self::carbon($trailer['lease_expires_at'] ?? null);
642
+ if (!$lease || $lease->gt($now->copy()->addDays(self::EXPIRING_DAYS))) {
643
+ continue;
644
+ }
645
+
646
+ $subject = ['type' => 'trailer', 'class' => $trailer['class'] ?? null, 'uuid' => $trailer['uuid'] ?? null, 'public_id' => $trailer['public_id'] ?? null, 'label' => $trailer['label'] ?? ($trailer['public_id'] ?? 'Trailer'), 'photo_url' => $trailer['photo_url'] ?? null];
647
+ $publicId = $trailer['public_id'] ?? ($trailer['uuid'] ?? '');
648
+ $items[] = self::leaseItem($subject, $lease, self::record('management.trailers.index.details', $trailer['public_id'] ?? null), ['type' => 'trailer', 'uuid' => $trailer['uuid'] ?? null, 'public_id' => $trailer['public_id'] ?? null], $now, $publicId);
649
+ }
650
+
651
+ return $items;
652
+ }
653
+
654
+ /**
655
+ * Devices not attached to anything.
656
+ */
657
+ public static function deviceItems(array $devices, Carbon $now): array
658
+ {
659
+ $items = [];
660
+
661
+ foreach ($devices as $device) {
662
+ if (!empty($device['attachable_uuid'])) {
663
+ continue;
664
+ }
665
+
666
+ $subject = ['type' => 'device', 'class' => $device['class'] ?? null, 'uuid' => $device['uuid'] ?? null, 'public_id' => $device['public_id'] ?? null, 'label' => $device['name'] ?? ($device['device_id'] ?? ($device['public_id'] ?? 'Device')), 'photo_url' => null];
667
+ $items[] = self::item('device_unattached', $subject, sprintf('%s not attached to a vehicle', $subject['label']), [
668
+ 'severity' => self::SEVERITY_INFO,
669
+ 'meta_line' => !empty($device['online']) ? 'online' : 'offline',
670
+ 'record' => self::record('connectivity.devices.index.details', $device['public_id'] ?? null),
671
+ 'source' => ['type' => 'device', 'uuid' => $device['uuid'] ?? null, 'public_id' => $device['public_id'] ?? null],
672
+ 'actions' => ['attach_device', 'acknowledge', 'snooze', 'assign', 'open_record'],
673
+ ], $now, $device['public_id'] ?? ($device['uuid'] ?? ''));
674
+ }
675
+
676
+ return $items;
677
+ }
678
+
679
+ /**
680
+ * Parts at or below their reorder point. Critical when there are none
681
+ * left and an open work order's checklist names the part.
682
+ */
683
+ public static function partItems(array $parts, array $workOrders, Carbon $now): array
684
+ {
685
+ $items = [];
686
+
687
+ foreach ($parts as $part) {
688
+ $onHand = (int) ($part['quantity_on_hand'] ?? 0);
689
+ $threshold = self::lowStockThreshold($part);
690
+ if ($onHand > $threshold) {
691
+ continue;
692
+ }
693
+
694
+ $name = $part['name'] ?? ($part['sku'] ?? 'Part');
695
+ $blocks = self::workOrderNaming($workOrders, [$part['name'] ?? null, $part['sku'] ?? null]);
696
+ $meta = [sprintf('reorder point %d', $threshold)];
697
+ if ($blocks) {
698
+ $meta[] = 'blocks ' . $blocks;
699
+ }
700
+
701
+ $subject = ['type' => 'part', 'class' => $part['class'] ?? null, 'uuid' => $part['uuid'] ?? null, 'public_id' => $part['public_id'] ?? null, 'label' => $name, 'photo_url' => $part['photo_url'] ?? null];
702
+ $items[] = self::item('part_low_stock', $subject, $onHand > 0
703
+ ? sprintf('%s — %d left, reorder point %d', $name, $onHand, $threshold)
704
+ : sprintf('%s — out of stock', $name), [
705
+ 'severity' => ($onHand <= 0 && $blocks) ? self::SEVERITY_CRITICAL : self::SEVERITY_WARNING,
706
+ 'meta_line' => implode(' · ', $meta),
707
+ 'record' => self::record('maintenance.parts.index.details', $part['public_id'] ?? null),
708
+ 'source' => ['type' => 'part', 'uuid' => $part['uuid'] ?? null, 'public_id' => $part['public_id'] ?? null, 'quantity_on_hand' => $onHand, 'threshold' => $threshold],
709
+ 'actions' => ['acknowledge', 'snooze', 'assign', 'open_record'],
710
+ ], $now, $part['public_id'] ?? ($part['uuid'] ?? ''));
711
+ }
712
+
713
+ return $items;
714
+ }
715
+
716
+ /**
717
+ * Fuel transactions the provider sync could not match to a vehicle.
718
+ */
719
+ public static function fuelItems(array $transactions, Carbon $now): array
720
+ {
721
+ $items = [];
722
+
723
+ foreach ($transactions as $transaction) {
724
+ if (($transaction['sync_status'] ?? 'unmatched') !== 'unmatched') {
725
+ continue;
726
+ }
727
+
728
+ $amount = self::money($transaction['amount'] ?? null, $transaction['currency'] ?? null);
729
+ $station = $transaction['station_name'] ?? ($transaction['provider'] ?? 'fuel provider');
730
+ $meta = [];
731
+ if (!empty($transaction['vehicle_card_id'])) {
732
+ $meta[] = 'card ' . $transaction['vehicle_card_id'];
733
+ }
734
+ if (!empty($transaction['plate_number'])) {
735
+ $meta[] = 'plate ' . $transaction['plate_number'];
736
+ }
737
+ if (!empty($transaction['suggested_vehicle']['label'])) {
738
+ $meta[] = 'suggest: ' . $transaction['suggested_vehicle']['label'];
739
+ }
740
+ $when = self::carbon($transaction['transaction_at'] ?? null);
741
+ if ($when) {
742
+ $meta[] = self::agoWords($when, $now);
743
+ }
744
+
745
+ $subject = ['type' => 'fuel_transaction', 'class' => $transaction['class'] ?? null, 'uuid' => $transaction['uuid'] ?? null, 'public_id' => $transaction['public_id'] ?? null, 'label' => $station, 'photo_url' => null];
746
+ $items[] = self::item('fuel_unmatched', $subject, sprintf('%s at %s — no vehicle matched', $amount, $station), [
747
+ 'severity' => self::SEVERITY_WARNING,
748
+ 'meta_line' => implode(' · ', $meta) ?: null,
749
+ 'record' => self::record('management.fuel-transactions.index.details', $transaction['public_id'] ?? null),
750
+ 'source' => ['type' => 'fuel_transaction', 'uuid' => $transaction['uuid'] ?? null, 'public_id' => $transaction['public_id'] ?? null, 'suggested_vehicle' => $transaction['suggested_vehicle'] ?? null],
751
+ 'actions' => ['match_vehicle', 'ignore_transaction', 'acknowledge', 'snooze', 'assign', 'open_record'],
752
+ ], $now, $transaction['public_id'] ?? ($transaction['uuid'] ?? ''));
753
+ }
754
+
755
+ return $items;
756
+ }
757
+
758
+ /**
759
+ * Notices are the one kind of item a person writes rather than a rule
760
+ * finds. They arrive as alert rows.
761
+ */
762
+ public static function noticeItems(array $notices, Carbon $now): array
763
+ {
764
+ $items = [];
765
+
766
+ foreach ($notices as $notice) {
767
+ if (($notice['status'] ?? 'open') === 'resolved') {
768
+ continue;
769
+ }
770
+
771
+ $due = self::carbon($notice['meta']['due_at'] ?? null);
772
+ $subject = $notice['subject'] ?? null;
773
+ $items[] = self::item('notice', $subject, (string) ($notice['message'] ?? 'Notice'), [
774
+ 'severity' => in_array($notice['severity'] ?? 'info', [self::SEVERITY_CRITICAL, self::SEVERITY_WARNING, self::SEVERITY_INFO], true) ? $notice['severity'] : self::SEVERITY_INFO,
775
+ 'due_at' => $due,
776
+ 'meta_line' => $notice['meta']['scope'] ?? null,
777
+ 'record' => null,
778
+ 'source' => ['type' => 'notice', 'uuid' => $notice['uuid'] ?? null, 'public_id' => $notice['public_id'] ?? null],
779
+ 'actions' => ['resolve', 'acknowledge', 'snooze', 'assign'],
780
+ 'state' => $notice['state'] ?? null,
781
+ ], $now, $notice['public_id'] ?? ($notice['uuid'] ?? ''));
782
+ }
783
+
784
+ return $items;
785
+ }
786
+
787
+ // ------------------------------------------------------------------
788
+ // State, filtering, sorting, counting
789
+ // ------------------------------------------------------------------
790
+
791
+ /**
792
+ * Attach each item's alert state, keyed by item key. Items with no row
793
+ * get the open default.
794
+ */
795
+ public static function mergeStates(array $items, array $states, Carbon $now): array
796
+ {
797
+ foreach ($items as &$item) {
798
+ $state = $item['state'] ?? ($states[$item['key']] ?? null);
799
+ $item['state'] = self::normalizeState($state, $now);
800
+ }
801
+ unset($item);
802
+
803
+ return $items;
804
+ }
805
+
806
+ /**
807
+ * A state row as the console reads it: the stored status, refined by
808
+ * whether the snooze has ended.
809
+ */
810
+ public static function normalizeState(?array $state, Carbon $now): array
811
+ {
812
+ $state = $state ?? [];
813
+ $until = self::carbon($state['snoozed_until'] ?? null);
814
+ $status = $state['status'] ?? 'open';
815
+
816
+ if ($until && $until->gt($now)) {
817
+ $status = 'snoozed';
818
+ } elseif ($status === 'snoozed') {
819
+ $status = !empty($state['acknowledged_at']) ? 'acknowledged' : 'open';
820
+ }
821
+
822
+ return [
823
+ 'status' => $status,
824
+ 'alert_id' => $state['alert_id'] ?? null,
825
+ 'acknowledged_at' => $state['acknowledged_at'] ?? null,
826
+ 'acknowledged_by_name' => $state['acknowledged_by_name'] ?? null,
827
+ 'snoozed_until' => $until && $until->gt($now) ? $until->toIso8601String() : null,
828
+ 'snoozed_by_name' => $until && $until->gt($now) ? ($state['snoozed_by_name'] ?? null) : null,
829
+ 'assigned_to' => $state['assigned_to'] ?? null,
830
+ 'planned_at' => $state['planned_at'] ?? null,
831
+ 'resolved_at' => $state['resolved_at'] ?? null,
832
+ 'resolved_by_name' => $state['resolved_by_name'] ?? null,
833
+ 'resolution' => $state['resolution'] ?? null,
834
+ 'triggered_at' => $state['triggered_at'] ?? null,
835
+ ];
836
+ }
837
+
838
+ public static function isOpen(array $item, Carbon $now): bool
839
+ {
840
+ return in_array($item['state']['status'] ?? 'open', ['open', 'acknowledged'], true);
841
+ }
842
+
843
+ public static function isSnoozed(array $item, Carbon $now): bool
844
+ {
845
+ return ($item['state']['status'] ?? 'open') === 'snoozed';
846
+ }
847
+
848
+ /**
849
+ * Keep the items for a status tab.
850
+ */
851
+ public static function forTab(array $items, string $tab, Carbon $now): array
852
+ {
853
+ return array_values(array_filter($items, function ($item) use ($tab, $now) {
854
+ return match ($tab) {
855
+ 'snoozed' => self::isSnoozed($item, $now),
856
+ 'resolved' => ($item['state']['status'] ?? null) === 'resolved',
857
+ default => self::isOpen($item, $now),
858
+ };
859
+ }));
860
+ }
861
+
862
+ /**
863
+ * Keep the items every one of the given pills matches (pills AND).
864
+ */
865
+ public static function forPills(array $items, array $pills): array
866
+ {
867
+ $pills = array_values(array_filter(array_map('trim', $pills), fn ($pill) => isset(self::PILLS[$pill])));
868
+ if (!$pills) {
869
+ return array_values($items);
870
+ }
871
+
872
+ return array_values(array_filter($items, function ($item) use ($pills) {
873
+ foreach ($pills as $pill) {
874
+ if (!in_array($pill, $item['pills'] ?? [], true)) {
875
+ return false;
876
+ }
877
+ }
878
+
879
+ return true;
880
+ }));
881
+ }
882
+
883
+ /**
884
+ * Keep the items whose title, subject, chip or meta line mentions the query.
885
+ */
886
+ public static function search(array $items, ?string $query): array
887
+ {
888
+ $query = trim((string) $query);
889
+ if ($query === '') {
890
+ return array_values($items);
891
+ }
892
+
893
+ $needle = Str::lower($query);
894
+
895
+ return array_values(array_filter($items, function ($item) use ($needle) {
896
+ $haystack = Str::lower(implode(' ', array_filter([
897
+ $item['title'] ?? '',
898
+ $item['subject']['label'] ?? '',
899
+ $item['subject']['public_id'] ?? '',
900
+ $item['chip'] ?? '',
901
+ $item['meta_line'] ?? '',
902
+ $item['rule'] ?? '',
903
+ ])));
904
+
905
+ return str_contains($haystack, $needle);
906
+ }));
907
+ }
908
+
909
+ /**
910
+ * Keep the items whose subject belongs to a fleet.
911
+ */
912
+ public static function forFleet(array $items, ?string $fleet): array
913
+ {
914
+ $fleet = trim((string) $fleet);
915
+ if ($fleet === '' || $fleet === 'all') {
916
+ return array_values($items);
917
+ }
918
+
919
+ return array_values(array_filter($items, fn ($item) => in_array($fleet, $item['subject']['fleets'] ?? [], true)));
920
+ }
921
+
922
+ /**
923
+ * Keep the items of one category.
924
+ */
925
+ public static function forCategory(array $items, ?string $category): array
926
+ {
927
+ $category = trim((string) $category);
928
+ if ($category === '') {
929
+ return array_values($items);
930
+ }
931
+
932
+ return array_values(array_filter($items, fn ($item) => ($item['category'] ?? null) === $category));
933
+ }
934
+
935
+ /**
936
+ * Keep the items assigned to a user.
937
+ */
938
+ public static function forAssignee(array $items, ?string $userUuid): array
939
+ {
940
+ if (!$userUuid) {
941
+ return [];
942
+ }
943
+
944
+ return array_values(array_filter($items, fn ($item) => ($item['state']['assigned_to']['uuid'] ?? null) === $userUuid));
945
+ }
946
+
947
+ /**
948
+ * Count the pills over a set of items.
949
+ */
950
+ public static function counts(array $items): array
951
+ {
952
+ $counts = array_fill_keys(array_keys(self::PILLS), 0);
953
+
954
+ foreach ($items as $item) {
955
+ foreach ($item['pills'] ?? [] as $pill) {
956
+ $counts[$pill]++;
957
+ }
958
+ }
959
+
960
+ return $counts;
961
+ }
962
+
963
+ /**
964
+ * The numbers the header and widget show.
965
+ */
966
+ public static function summary(array $items, Carbon $now): array
967
+ {
968
+ $open = array_filter($items, fn ($item) => self::isOpen($item, $now));
969
+ $snoozed = array_filter($items, fn ($item) => self::isSnoozed($item, $now));
970
+
971
+ return [
972
+ 'open' => count($open),
973
+ 'acknowledged' => count(array_filter($open, fn ($item) => $item['state']['status'] === 'acknowledged')),
974
+ 'snoozed' => count($snoozed),
975
+ 'overdue' => count(array_filter($open, fn ($item) => $item['due_bucket'] === 'overdue')),
976
+ 'critical' => count(array_filter($open, fn ($item) => $item['severity'] === self::SEVERITY_CRITICAL)),
977
+ 'undated' => count(array_filter($open, fn ($item) => $item['due_bucket'] === 'none')),
978
+ ];
979
+ }
980
+
981
+ /**
982
+ * Overdue first (oldest first), then today, this week and later by due
983
+ * date, then undated by severity.
984
+ */
985
+ public static function sort(array $items): array
986
+ {
987
+ $bucketOrder = ['overdue' => 0, 'today' => 1, 'week' => 2, 'later' => 3, 'none' => 4];
988
+ $severityOrder = [self::SEVERITY_CRITICAL => 0, self::SEVERITY_WARNING => 1, self::SEVERITY_INFO => 2];
989
+
990
+ usort($items, function ($a, $b) use ($bucketOrder, $severityOrder) {
991
+ $bucket = ($bucketOrder[$a['due_bucket'] ?? 'none'] ?? 9) <=> ($bucketOrder[$b['due_bucket'] ?? 'none'] ?? 9);
992
+ if ($bucket !== 0) {
993
+ return $bucket;
994
+ }
995
+
996
+ if (($a['due_bucket'] ?? 'none') !== 'none') {
997
+ $due = strcmp((string) $a['due_at'], (string) $b['due_at']);
998
+ if ($due !== 0) {
999
+ return $due;
1000
+ }
1001
+ }
1002
+
1003
+ $severity = ($severityOrder[$a['severity'] ?? ''] ?? 9) <=> ($severityOrder[$b['severity'] ?? ''] ?? 9);
1004
+ if ($severity !== 0) {
1005
+ return $severity;
1006
+ }
1007
+
1008
+ return strcmp((string) ($a['title'] ?? ''), (string) ($b['title'] ?? ''));
1009
+ });
1010
+
1011
+ return array_values($items);
1012
+ }
1013
+
1014
+ /**
1015
+ * Group sorted items by due bucket, in display order, dropping empty groups.
1016
+ */
1017
+ public static function group(array $items): array
1018
+ {
1019
+ $groups = [];
1020
+ foreach (['overdue', 'today', 'week', 'later', 'none'] as $bucket) {
1021
+ $members = array_values(array_filter($items, fn ($item) => ($item['due_bucket'] ?? 'none') === $bucket));
1022
+ if ($members) {
1023
+ $groups[] = ['key' => $bucket, 'count' => count($members), 'items' => $members];
1024
+ }
1025
+ }
1026
+
1027
+ return $groups;
1028
+ }
1029
+
1030
+ /**
1031
+ * A page of items.
1032
+ */
1033
+ public static function paginate(array $items, int $page, int $limit): array
1034
+ {
1035
+ $page = max(1, $page);
1036
+ $limit = max(1, min(200, $limit));
1037
+ $total = count($items);
1038
+
1039
+ return [
1040
+ 'items' => array_slice(array_values($items), ($page - 1) * $limit, $limit),
1041
+ 'meta' => ['total' => $total, 'page' => $page, 'limit' => $limit, 'pages' => (int) ceil($total / $limit)],
1042
+ ];
1043
+ }
1044
+
1045
+ // ------------------------------------------------------------------
1046
+ // Helpers
1047
+ // ------------------------------------------------------------------
1048
+
1049
+ /**
1050
+ * The stable key for a rule applied to a record.
1051
+ */
1052
+ public static function keyFor(string $rule, string $publicId): string
1053
+ {
1054
+ return $rule . ':' . $publicId;
1055
+ }
1056
+
1057
+ /**
1058
+ * Split a key back into its rule and public id.
1059
+ *
1060
+ * @return array{0: string, 1: string}|null
1061
+ */
1062
+ public static function parseKey(?string $key): ?array
1063
+ {
1064
+ $key = (string) $key;
1065
+ $at = strpos($key, ':');
1066
+ if ($at === false) {
1067
+ return null;
1068
+ }
1069
+
1070
+ $rule = substr($key, 0, $at);
1071
+ $publicId = substr($key, $at + 1);
1072
+
1073
+ if (!isset(self::RULES[$rule]) || $publicId === '') {
1074
+ return null;
1075
+ }
1076
+
1077
+ return [$rule, $publicId];
1078
+ }
1079
+
1080
+ /**
1081
+ * The rules a state row can carry: every rule but the manual notice,
1082
+ * which is a row already.
1083
+ */
1084
+ public static function stateTypes(): array
1085
+ {
1086
+ return array_values(array_filter(array_keys(self::RULES), fn ($rule) => $rule !== 'notice'));
1087
+ }
1088
+
1089
+ /**
1090
+ * The threshold a part is low at: the reorder point when set, else the
1091
+ * spec's low-stock threshold, else the default.
1092
+ */
1093
+ public static function lowStockThreshold(array $part): int
1094
+ {
1095
+ $reorderPoint = (int) ($part['reorder_point'] ?? 0);
1096
+ if ($reorderPoint > 0) {
1097
+ return $reorderPoint;
1098
+ }
1099
+
1100
+ $specs = $part['specs'] ?? [];
1101
+ if (is_string($specs)) {
1102
+ $specs = json_decode($specs, true) ?: [];
1103
+ }
1104
+ $specThreshold = (int) ($specs['low_stock_threshold'] ?? 0);
1105
+
1106
+ return $specThreshold > 0 ? $specThreshold : self::DEFAULT_LOW_STOCK;
1107
+ }
1108
+
1109
+ /**
1110
+ * Which due bucket a date falls in.
1111
+ */
1112
+ public static function bucket(?Carbon $due, Carbon $now): string
1113
+ {
1114
+ if (!$due) {
1115
+ return 'none';
1116
+ }
1117
+ if ($due->lt($now)) {
1118
+ return 'overdue';
1119
+ }
1120
+ if ($due->isSameDay($now)) {
1121
+ return 'today';
1122
+ }
1123
+ if ($due->lte($now->copy()->addDays(self::DUE_SOON_DAYS))) {
1124
+ return 'week';
1125
+ }
1126
+
1127
+ return 'later';
1128
+ }
1129
+
1130
+ /**
1131
+ * The short due text a row shows.
1132
+ */
1133
+ public static function dueLabel(?Carbon $due, Carbon $now): ?string
1134
+ {
1135
+ if (!$due) {
1136
+ return null;
1137
+ }
1138
+
1139
+ return match (self::bucket($due, $now)) {
1140
+ 'overdue' => self::overdueWords($due, $now),
1141
+ 'today' => 'Today ' . $due->format('H:i'),
1142
+ 'week' => $due->format('D j M'),
1143
+ default => $due->format('j M'),
1144
+ };
1145
+ }
1146
+
1147
+ public static function overdueWords(Carbon $due, Carbon $now): string
1148
+ {
1149
+ $minutes = $due->diffInMinutes($now);
1150
+ if ($minutes < 60) {
1151
+ return max(1, $minutes) . 'm overdue';
1152
+ }
1153
+ $hours = $due->diffInHours($now);
1154
+ if ($hours < 24) {
1155
+ return $hours . 'h overdue';
1156
+ }
1157
+
1158
+ return $due->diffInDays($now) . 'd overdue';
1159
+ }
1160
+
1161
+ public static function dueInWords(Carbon $due, Carbon $now): string
1162
+ {
1163
+ if ($due->isSameDay($now)) {
1164
+ return 'today';
1165
+ }
1166
+ if ($due->isSameDay($now->copy()->addDay())) {
1167
+ return 'tomorrow';
1168
+ }
1169
+
1170
+ $days = $now->copy()->startOfDay()->diffInDays($due->copy()->startOfDay());
1171
+ if ($days <= self::DUE_SOON_DAYS) {
1172
+ return sprintf('in %d day%s', $days, $days === 1 ? '' : 's');
1173
+ }
1174
+
1175
+ return $due->format('j M');
1176
+ }
1177
+
1178
+ /**
1179
+ * "35m ago", "5h ago", "2d ago".
1180
+ */
1181
+ public static function agoWords(Carbon $then, Carbon $now): string
1182
+ {
1183
+ $minutes = max(0, $then->diffInMinutes($now, false));
1184
+ if ($minutes < 60) {
1185
+ return max(1, $minutes) . 'm ago';
1186
+ }
1187
+ if ($minutes < 1440) {
1188
+ return intdiv($minutes, 60) . 'h ago';
1189
+ }
1190
+
1191
+ return intdiv($minutes, 1440) . 'd ago';
1192
+ }
1193
+
1194
+ public static function minutesWords(int $minutes): string
1195
+ {
1196
+ $minutes = max(0, $minutes);
1197
+ if ($minutes < 60) {
1198
+ return $minutes . 'm';
1199
+ }
1200
+
1201
+ $hours = intdiv($minutes, 60);
1202
+ $rest = $minutes % 60;
1203
+
1204
+ return $rest ? sprintf('%dh %dm', $hours, $rest) : $hours . 'h';
1205
+ }
1206
+
1207
+ /**
1208
+ * Accepts a Carbon, a DateTime, or a string; anything else is null.
1209
+ */
1210
+ public static function carbon(mixed $value): ?Carbon
1211
+ {
1212
+ if ($value instanceof Carbon) {
1213
+ return $value->copy();
1214
+ }
1215
+ if ($value instanceof \DateTimeInterface) {
1216
+ return Carbon::instance($value);
1217
+ }
1218
+ if (is_string($value) && trim($value) !== '') {
1219
+ try {
1220
+ return Carbon::parse($value);
1221
+ } catch (\Throwable) {
1222
+ return null;
1223
+ }
1224
+ }
1225
+
1226
+ return null;
1227
+ }
1228
+
1229
+ /**
1230
+ * The route the "open record" link goes to.
1231
+ */
1232
+ public static function record(?string $route, ?string $model): ?array
1233
+ {
1234
+ if (!$route || !$model) {
1235
+ return null;
1236
+ }
1237
+
1238
+ return ['route' => $route, 'model' => $model];
1239
+ }
1240
+
1241
+ protected static function item(string $rule, ?array $subject, string $title, array $extra, Carbon $now, string $publicId): array
1242
+ {
1243
+ $meta = self::RULES[$rule];
1244
+ $due = self::carbon($extra['due_at'] ?? null);
1245
+ $bucket = self::bucket($due, $now);
1246
+ $subject = $subject ? self::subject($subject) : null;
1247
+ $item = [
1248
+ 'key' => self::keyFor($rule, $publicId),
1249
+ 'rule' => $rule,
1250
+ 'category' => $meta['category'],
1251
+ 'lane' => $due || !empty($extra['window']) ? $meta['lane'] : self::LANE_ANYTIME,
1252
+ 'chip' => $meta['chip'],
1253
+ 'severity' => $extra['severity'] ?? self::SEVERITY_INFO,
1254
+ 'title' => $title,
1255
+ 'subject' => $subject,
1256
+ 'meta_line' => $extra['meta_line'] ?? null,
1257
+ 'due_at' => $due?->toIso8601String(),
1258
+ 'due_bucket' => $bucket,
1259
+ 'due_label' => self::dueLabel($due, $now),
1260
+ 'window' => $extra['window'] ?? null,
1261
+ 'planned_at' => null,
1262
+ 'record' => $extra['record'] ?? null,
1263
+ 'source' => $extra['source'] ?? null,
1264
+ 'details' => $extra['details'] ?? null,
1265
+ 'actions' => array_values($extra['actions'] ?? ['acknowledge', 'snooze', 'assign']),
1266
+ 'state' => $extra['state'] ?? null,
1267
+ ];
1268
+
1269
+ $item['pills'] = self::pillsFor($item);
1270
+
1271
+ return $item;
1272
+ }
1273
+
1274
+ protected static function pillsFor(array $item): array
1275
+ {
1276
+ $pills = [];
1277
+ foreach (self::PILLS as $pill => $definition) {
1278
+ if (in_array($item['rule'], $definition['rules'] ?? [], true) || in_array($item['due_bucket'], $definition['buckets'] ?? [], true)) {
1279
+ $pills[] = $pill;
1280
+ }
1281
+ }
1282
+
1283
+ return $pills;
1284
+ }
1285
+
1286
+ protected static function subject(array $subject): array
1287
+ {
1288
+ return [
1289
+ 'type' => $subject['type'] ?? null,
1290
+ 'class' => $subject['class'] ?? null,
1291
+ 'uuid' => $subject['uuid'] ?? null,
1292
+ 'public_id' => $subject['public_id'] ?? null,
1293
+ 'label' => $subject['label'] ?? ($subject['public_id'] ?? ''),
1294
+ 'photo_url' => $subject['photo_url'] ?? null,
1295
+ 'phone' => $subject['phone'] ?? null,
1296
+ 'fleets' => array_values($subject['fleets'] ?? []),
1297
+ ];
1298
+ }
1299
+
1300
+ protected static function driverSubject(array $driver): array
1301
+ {
1302
+ return [
1303
+ 'type' => 'driver',
1304
+ 'class' => $driver['class'] ?? null,
1305
+ 'uuid' => $driver['uuid'] ?? null,
1306
+ 'public_id' => $driver['public_id'] ?? null,
1307
+ 'label' => $driver['name'] ?? ($driver['label'] ?? ($driver['public_id'] ?? 'Driver')),
1308
+ 'photo_url' => $driver['photo_url'] ?? null,
1309
+ 'phone' => $driver['phone'] ?? null,
1310
+ 'fleets' => $driver['fleets'] ?? [],
1311
+ ];
1312
+ }
1313
+
1314
+ protected static function vehicleSubject(array $vehicle): array
1315
+ {
1316
+ return [
1317
+ 'type' => 'vehicle',
1318
+ 'class' => $vehicle['class'] ?? null,
1319
+ 'uuid' => $vehicle['uuid'] ?? null,
1320
+ 'public_id' => $vehicle['public_id'] ?? null,
1321
+ 'label' => $vehicle['label'] ?? ($vehicle['display_name'] ?? ($vehicle['public_id'] ?? 'Vehicle')),
1322
+ 'photo_url' => $vehicle['photo_url'] ?? null,
1323
+ 'fleets' => $vehicle['fleets'] ?? [],
1324
+ ];
1325
+ }
1326
+
1327
+ protected static function leaseItem(array $subject, Carbon $lease, ?array $record, array $source, Carbon $now, string $publicId): array
1328
+ {
1329
+ $expired = $lease->lt($now->copy()->startOfDay());
1330
+
1331
+ return self::item('lease_expiring', $subject, $expired
1332
+ ? sprintf('%s lease ended %s', $subject['label'], $lease->format('j M'))
1333
+ : sprintf('%s lease ends %s', $subject['label'], self::dueInWords($lease, $now)), [
1334
+ 'severity' => $expired ? self::SEVERITY_CRITICAL : self::SEVERITY_WARNING,
1335
+ 'due_at' => $lease,
1336
+ 'record' => $record,
1337
+ 'source' => $source,
1338
+ 'actions' => ['acknowledge', 'snooze', 'assign', 'open_record'],
1339
+ ], $now, $publicId);
1340
+ }
1341
+
1342
+ /**
1343
+ * The code of the first open work order whose checklist names any of the
1344
+ * given words, or null.
1345
+ */
1346
+ protected static function workOrderNaming(array $workOrders, array $words): ?string
1347
+ {
1348
+ $words = array_values(array_filter(array_map(fn ($word) => Str::lower(trim((string) $word)), $words)));
1349
+ if (!$words) {
1350
+ return null;
1351
+ }
1352
+
1353
+ foreach ($workOrders as $workOrder) {
1354
+ if (in_array($workOrder['status'] ?? 'open', self::WORK_ORDER_CLOSED, true)) {
1355
+ continue;
1356
+ }
1357
+
1358
+ $checklist = $workOrder['checklist'] ?? [];
1359
+ if (is_string($checklist)) {
1360
+ $checklist = json_decode($checklist, true) ?: [];
1361
+ }
1362
+
1363
+ $text = Str::lower(implode(' ', array_map(fn ($entry) => is_array($entry) ? ($entry['title'] ?? '') : (string) $entry, $checklist)) . ' ' . ($workOrder['subject'] ?? ''));
1364
+
1365
+ foreach ($words as $word) {
1366
+ if ($word !== '' && str_contains($text, $word)) {
1367
+ return $workOrder['code'] ?? ($workOrder['public_id'] ?? null);
1368
+ }
1369
+ }
1370
+ }
1371
+
1372
+ return null;
1373
+ }
1374
+
1375
+ /**
1376
+ * The company vehicle a fuel transaction's identity columns point at, or
1377
+ * null. Reports which field matched so the console can say why.
1378
+ *
1379
+ * @param array $vehicles rows with uuid, public_id, label, plate_number, vin, fuel_card_number
1380
+ */
1381
+ public static function suggestVehicleForFuel(array $transaction, array $vehicles): ?array
1382
+ {
1383
+ $candidates = [
1384
+ 'fuel_card_number' => $transaction['vehicle_card_id'] ?? null,
1385
+ 'plate_number' => $transaction['plate_number'] ?? null,
1386
+ 'vin' => $transaction['vin'] ?? null,
1387
+ ];
1388
+
1389
+ foreach ($candidates as $field => $value) {
1390
+ $value = Str::lower(trim((string) $value));
1391
+ if ($value === '') {
1392
+ continue;
1393
+ }
1394
+
1395
+ foreach ($vehicles as $vehicle) {
1396
+ if (Str::lower(trim((string) ($vehicle[$field] ?? ''))) === $value) {
1397
+ return [
1398
+ 'uuid' => $vehicle['uuid'] ?? null,
1399
+ 'public_id' => $vehicle['public_id'] ?? null,
1400
+ 'label' => $vehicle['label'] ?? ($vehicle['public_id'] ?? ''),
1401
+ 'matched' => $field,
1402
+ ];
1403
+ }
1404
+ }
1405
+ }
1406
+
1407
+ return null;
1408
+ }
1409
+
1410
+ protected static function money(mixed $amount, ?string $currency): string
1411
+ {
1412
+ if ($amount === null || $amount === '') {
1413
+ return 'Fuel purchase';
1414
+ }
1415
+
1416
+ $value = (float) $amount;
1417
+ // Provider amounts are stored in minor units when they are integers.
1418
+ if (is_int($amount) || (is_string($amount) && ctype_digit($amount))) {
1419
+ $value = $value / 100;
1420
+ }
1421
+
1422
+ return trim(($currency ? strtoupper($currency) . ' ' : '') . number_format($value, 2));
1423
+ }
1424
+ }