@fleetbase/fleetops-engine 0.6.64 → 0.6.65

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 (284) hide show
  1. package/RELEASE.md +7 -5
  2. package/addon/components/attachment-list.hbs +65 -0
  3. package/addon/components/attachment-list.js +8 -0
  4. package/addon/components/cell/attached-vehicle.hbs +1 -1
  5. package/addon/components/cell/attached-vehicle.js +3 -3
  6. package/addon/components/cell/driver-identity.js +4 -4
  7. package/addon/components/cell/driver-name.hbs +1 -1
  8. package/addon/components/cell/translated-value.hbs +9 -0
  9. package/addon/components/cell/translated-value.js +57 -0
  10. package/addon/components/contact/card.hbs +58 -0
  11. package/addon/components/contact/card.js +19 -0
  12. package/addon/components/contact/form.hbs +1 -1
  13. package/addon/components/contact/panel-header.hbs +1 -1
  14. package/addon/components/customer/form.hbs +2 -2
  15. package/addon/components/device/manager.hbs +26 -28
  16. package/addon/components/device/manager.js +28 -2
  17. package/addon/components/device/panel-tabs/vehicle.hbs +13 -13
  18. package/addon/components/device/panel-tabs/vehicle.js +8 -0
  19. package/addon/components/driver/card.hbs +24 -13
  20. package/addon/components/driver/card.js +20 -6
  21. package/addon/components/driver/form.hbs +3 -3
  22. package/addon/components/driver/panel-header.hbs +2 -2
  23. package/addon/components/driver/pill.hbs +2 -2
  24. package/addon/components/equipment/form.js +11 -4
  25. package/addon/components/equipment/pill.hbs +20 -0
  26. package/addon/components/equipment/pill.js +3 -0
  27. package/addon/components/filter/multi-model.hbs +19 -0
  28. package/addon/components/filter/multi-model.js +90 -0
  29. package/addon/components/fleet/driver-listing.hbs +1 -1
  30. package/addon/components/fleet/vehicle-listing.hbs +1 -1
  31. package/addon/components/fleet-panel/driver-listing.hbs +1 -1
  32. package/addon/components/fleet-panel/vehicle-listing.hbs +1 -1
  33. package/addon/components/layout/fleet-ops-sidebar.js +9 -7
  34. package/addon/components/maintenance/form.js +12 -4
  35. package/addon/components/maintenance-schedule/form.js +13 -7
  36. package/addon/components/map/leaflet-live-map.hbs +52 -0
  37. package/addon/components/map/order-list-overlay/order.hbs +4 -4
  38. package/addon/components/modals/attach-equipment.hbs +22 -0
  39. package/addon/components/modals/attach-equipment.js +3 -0
  40. package/addon/components/modals/attach-telematic-device.hbs +13 -8
  41. package/addon/components/modals/attach-telematic-device.js +11 -1
  42. package/addon/components/modals/attach-trailer.hbs +30 -0
  43. package/addon/components/modals/attach-trailer.js +3 -0
  44. package/addon/components/modals/contact-form.hbs +2 -2
  45. package/addon/components/modals/convert-customer-to-vendor.hbs +1 -1
  46. package/addon/components/modals/driver-form.hbs +2 -2
  47. package/addon/components/modals/select-trailer.hbs +35 -0
  48. package/addon/components/modals/select-trailer.js +3 -0
  49. package/addon/components/modals/vehicle-form.hbs +2 -2
  50. package/addon/components/orchestrator/resource-panel.hbs +2 -2
  51. package/addon/components/order/customer-avatar-stack.hbs +2 -2
  52. package/addon/components/order/details/detail.hbs +4 -4
  53. package/addon/components/order/details/payload.hbs +2 -2
  54. package/addon/components/order/kanban-card.hbs +4 -4
  55. package/addon/components/order-list-overlay/order.hbs +4 -4
  56. package/addon/components/trailer/card.hbs +68 -0
  57. package/addon/components/trailer/card.js +22 -0
  58. package/addon/components/trailer/details/connections.hbs +57 -0
  59. package/addon/components/trailer/details/connections.js +59 -0
  60. package/addon/components/trailer/details/equipment.hbs +25 -0
  61. package/addon/components/trailer/details/equipment.js +52 -0
  62. package/addon/components/trailer/details/maintenance-history.hbs +47 -0
  63. package/addon/components/trailer/details/maintenance-history.js +40 -0
  64. package/addon/components/trailer/details/schedules.hbs +45 -0
  65. package/addon/components/trailer/details/schedules.js +40 -0
  66. package/addon/components/trailer/details/work-orders.hbs +47 -0
  67. package/addon/components/trailer/details/work-orders.js +40 -0
  68. package/addon/components/trailer/details.hbs +399 -0
  69. package/addon/components/trailer/details.js +118 -0
  70. package/addon/components/trailer/form.hbs +447 -0
  71. package/addon/components/trailer/form.js +165 -0
  72. package/addon/components/trailer/panel-header.hbs +45 -0
  73. package/addon/components/trailer/panel-header.js +3 -0
  74. package/addon/components/trailer/pill.hbs +21 -0
  75. package/addon/components/trailer/pill.js +3 -0
  76. package/addon/components/vehicle/card.hbs +42 -11
  77. package/addon/components/vehicle/card.js +31 -0
  78. package/addon/components/vehicle/details/equipment.hbs +25 -0
  79. package/addon/components/vehicle/details/equipment.js +55 -0
  80. package/addon/components/vehicle/details/trailers.hbs +35 -0
  81. package/addon/components/vehicle/details/trailers.js +55 -0
  82. package/addon/components/vehicle/form.hbs +2 -2
  83. package/addon/components/vehicle/panel-header.hbs +2 -2
  84. package/addon/components/vehicle/pill.hbs +2 -2
  85. package/addon/components/vendor/card.hbs +58 -0
  86. package/addon/components/vendor/card.js +6 -0
  87. package/addon/components/vendor/panel-header.hbs +2 -2
  88. package/addon/components/vendor/personnel-manager.hbs +2 -2
  89. package/addon/components/work-order/form.js +12 -4
  90. package/addon/controllers/connectivity/devices/index/details/vehicle.js +11 -0
  91. package/addon/controllers/connectivity/devices/index/details.js +3 -3
  92. package/addon/controllers/connectivity/devices/index.js +1 -0
  93. package/addon/controllers/connectivity/telematics/details/attachments.js +28 -47
  94. package/addon/controllers/connectivity/telematics/details/devices.js +1 -0
  95. package/addon/controllers/management/contacts/customers.js +4 -0
  96. package/addon/controllers/management/contacts/index.js +32 -0
  97. package/addon/controllers/management/trailers/index/details.js +119 -0
  98. package/addon/controllers/management/trailers/index/edit.js +67 -0
  99. package/addon/controllers/management/trailers/index/new.js +43 -0
  100. package/addon/controllers/management/trailers/index.js +599 -0
  101. package/addon/controllers/management/vehicles/index/details.js +16 -0
  102. package/addon/controllers/management/vehicles/index.js +78 -4
  103. package/addon/controllers/management/vendors/index.js +32 -0
  104. package/addon/controllers/operations/orders/index.js +3 -1
  105. package/addon/controllers/settings/custom-fields.js +8 -0
  106. package/addon/extension.js +7 -0
  107. package/addon/helpers/placeholder-image.js +6 -0
  108. package/addon/helpers/resource-image.js +10 -0
  109. package/addon/helpers/trailer-placeholder-image.js +6 -0
  110. package/addon/routes/management/trailers/index/details/connections.js +2 -0
  111. package/addon/routes/management/trailers/index/details/devices.js +2 -0
  112. package/addon/routes/management/trailers/index/details/equipment.js +2 -0
  113. package/addon/routes/management/trailers/index/details/index.js +2 -0
  114. package/addon/routes/management/trailers/index/details/maintenance-history.js +2 -0
  115. package/addon/routes/management/trailers/index/details/positions.js +2 -0
  116. package/addon/routes/management/trailers/index/details/schedules.js +2 -0
  117. package/addon/routes/management/trailers/index/details/work-orders.js +2 -0
  118. package/addon/routes/management/trailers/index/details.js +30 -0
  119. package/addon/routes/management/trailers/index/edit.js +23 -0
  120. package/addon/routes/management/trailers/index/new.js +14 -0
  121. package/addon/routes/management/trailers/index.js +42 -0
  122. package/addon/routes/management/vehicles/index/details/trailers.js +2 -0
  123. package/addon/routes/management/vehicles/index.js +7 -1
  124. package/addon/routes.js +17 -0
  125. package/addon/services/device-actions.js +26 -16
  126. package/addon/services/trailer-actions.js +394 -0
  127. package/addon/services/vehicle-actions.js +111 -0
  128. package/addon/styles/fleetops-engine.css +349 -0
  129. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +19 -19
  130. package/addon/templates/connectivity/telematics/details/attachments.hbs +15 -15
  131. package/addon/templates/management/contacts/customers.hbs +48 -29
  132. package/addon/templates/management/contacts/index.hbs +52 -33
  133. package/addon/templates/management/trailers/index/details/connections.hbs +1 -0
  134. package/addon/templates/management/trailers/index/details/devices.hbs +1 -0
  135. package/addon/templates/management/trailers/index/details/equipment.hbs +1 -0
  136. package/addon/templates/management/trailers/index/details/index.hbs +1 -0
  137. package/addon/templates/management/trailers/index/details/maintenance-history.hbs +1 -0
  138. package/addon/templates/management/trailers/index/details/positions.hbs +1 -0
  139. package/addon/templates/management/trailers/index/details/schedules.hbs +1 -0
  140. package/addon/templates/management/trailers/index/details/work-orders.hbs +1 -0
  141. package/addon/templates/management/trailers/index/details.hbs +15 -0
  142. package/addon/templates/management/trailers/index/edit.hbs +12 -0
  143. package/addon/templates/management/trailers/index/new.hbs +11 -0
  144. package/addon/templates/management/trailers/index.hbs +67 -0
  145. package/addon/templates/management/vehicles/index/details/equipment.hbs +1 -1
  146. package/addon/templates/management/vehicles/index/details/trailers.hbs +1 -0
  147. package/addon/templates/management/vehicles/index/details.hbs +1 -1
  148. package/addon/templates/management/vendors/index.hbs +66 -32
  149. package/addon/utils/device-table-columns.js +10 -6
  150. package/addon/utils/live-map-card-content.js +56 -0
  151. package/addon/utils/placeholder-images.js +71 -0
  152. package/addon/utils/trailer-placeholder-image.js +11 -0
  153. package/app/components/attachment-list.js +1 -0
  154. package/app/components/cell/translated-value.js +1 -0
  155. package/app/components/contact/card.js +1 -0
  156. package/app/components/equipment/pill.js +1 -0
  157. package/app/components/filter/multi-model.js +1 -0
  158. package/app/components/modals/attach-equipment.js +1 -0
  159. package/app/components/modals/attach-trailer.js +1 -0
  160. package/app/components/modals/select-trailer.js +1 -0
  161. package/app/components/trailer/card.js +1 -0
  162. package/app/components/trailer/details/connections.js +1 -0
  163. package/app/components/trailer/details/equipment.js +1 -0
  164. package/app/components/trailer/details/maintenance-history.js +1 -0
  165. package/app/components/trailer/details/schedules.js +1 -0
  166. package/app/components/trailer/details/work-orders.js +1 -0
  167. package/app/components/trailer/details.js +1 -0
  168. package/app/components/trailer/form.js +1 -0
  169. package/app/components/trailer/panel-header.js +1 -0
  170. package/app/components/trailer/pill.js +1 -0
  171. package/app/components/vehicle/details/equipment.js +1 -0
  172. package/app/components/vehicle/details/trailers.js +1 -0
  173. package/app/components/vendor/card.js +1 -0
  174. package/app/controllers/management/trailers/index/details.js +1 -0
  175. package/app/controllers/management/trailers/index/edit.js +1 -0
  176. package/app/controllers/management/trailers/index/new.js +1 -0
  177. package/app/controllers/management/trailers/index.js +1 -0
  178. package/app/helpers/placeholder-image.js +1 -0
  179. package/app/helpers/resource-image.js +1 -0
  180. package/app/helpers/trailer-placeholder-image.js +1 -0
  181. package/app/routes/management/trailers/index/details/connections.js +1 -0
  182. package/app/routes/management/trailers/index/details/devices.js +1 -0
  183. package/app/routes/management/trailers/index/details/equipment.js +1 -0
  184. package/app/routes/management/trailers/index/details/index.js +1 -0
  185. package/app/routes/management/trailers/index/details/maintenance-history.js +1 -0
  186. package/app/routes/management/trailers/index/details/positions.js +1 -0
  187. package/app/routes/management/trailers/index/details/schedules.js +1 -0
  188. package/app/routes/management/trailers/index/details/work-orders.js +1 -0
  189. package/app/routes/management/trailers/index/details.js +1 -0
  190. package/app/routes/management/trailers/index/edit.js +1 -0
  191. package/app/routes/management/trailers/index/new.js +1 -0
  192. package/app/routes/management/trailers/index.js +1 -0
  193. package/app/routes/management/vehicles/index/details/trailers.js +1 -0
  194. package/app/services/trailer-actions.js +1 -0
  195. package/app/templates/management/trailers/index/details/connections.js +1 -0
  196. package/app/templates/management/trailers/index/details/devices.js +1 -0
  197. package/app/templates/management/trailers/index/details/equipment.js +1 -0
  198. package/app/templates/management/trailers/index/details/index.js +1 -0
  199. package/app/templates/management/trailers/index/details/maintenance-history.js +1 -0
  200. package/app/templates/management/trailers/index/details/positions.js +1 -0
  201. package/app/templates/management/trailers/index/details/schedules.js +1 -0
  202. package/app/templates/management/trailers/index/details/work-orders.js +1 -0
  203. package/app/templates/management/trailers/index/details.js +1 -0
  204. package/app/templates/management/trailers/index/edit.js +1 -0
  205. package/app/templates/management/trailers/index/new.js +1 -0
  206. package/app/templates/management/trailers/index.js +1 -0
  207. package/app/templates/management/vehicles/index/details/trailers.js +1 -0
  208. package/composer.json +1 -1
  209. package/extension.json +1 -1
  210. package/package.json +1 -1
  211. package/server/migrations/2026_09_03_000001_add_first_class_trailer_columns_to_assets_table.php +58 -0
  212. package/server/migrations/2026_09_03_000002_create_asset_connections_table.php +43 -0
  213. package/server/migrations/2026_09_03_000003_create_device_installations_table.php +33 -0
  214. package/server/src/Auth/Schemas/FleetOps.php +7 -0
  215. package/server/src/Events/TrailerLocationChanged.php +70 -0
  216. package/server/src/Exceptions/DeviceAlreadyAttachedException.php +18 -0
  217. package/server/src/Exports/TrailerExport.php +31 -0
  218. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +8 -2
  219. package/server/src/Http/Controllers/Api/v1/DeviceController.php +22 -15
  220. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +47 -0
  221. package/server/src/Http/Controllers/Api/v1/TrailerController.php +362 -0
  222. package/server/src/Http/Controllers/Api/v1/VehicleController.php +1 -0
  223. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +3 -0
  224. package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
  225. package/server/src/Http/Controllers/Internal/v1/SearchController.php +16 -0
  226. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +360 -0
  227. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +74 -0
  228. package/server/src/Http/Filter/EquipmentFilter.php +51 -0
  229. package/server/src/Http/Filter/OrderFilter.php +15 -0
  230. package/server/src/Http/Filter/TrailerFilter.php +269 -0
  231. package/server/src/Http/Filter/VehicleFilter.php +70 -3
  232. package/server/src/Http/Requests/CreateDeviceRequest.php +1 -1
  233. package/server/src/Http/Requests/CreateEquipmentRequest.php +1 -1
  234. package/server/src/Http/Requests/CreateTrailerRequest.php +54 -0
  235. package/server/src/Http/Requests/UpdateTrailerRequest.php +7 -0
  236. package/server/src/Http/Resources/v1/AssetConnection.php +90 -0
  237. package/server/src/Http/Resources/v1/Device.php +3 -1
  238. package/server/src/Http/Resources/v1/Equipment.php +3 -1
  239. package/server/src/Http/Resources/v1/Index/Vehicle.php +10 -0
  240. package/server/src/Http/Resources/v1/Trailer.php +199 -0
  241. package/server/src/Http/Resources/v1/Vehicle.php +2 -0
  242. package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +3 -0
  243. package/server/src/Imports/TrailerImport.php +33 -0
  244. package/server/src/Models/Asset.php +39 -10
  245. package/server/src/Models/AssetConnection.php +43 -0
  246. package/server/src/Models/Device.php +39 -8
  247. package/server/src/Models/DeviceInstallation.php +27 -0
  248. package/server/src/Models/Equipment.php +2 -0
  249. package/server/src/Models/Trailer.php +181 -0
  250. package/server/src/Models/Vehicle.php +13 -0
  251. package/server/src/Providers/FleetOpsServiceProvider.php +4 -2
  252. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +22 -0
  253. package/server/src/Support/Reporting/FleetOpsReportSchema.php +38 -0
  254. package/server/src/Support/Telematics/TelematicService.php +35 -0
  255. package/server/src/Support/TrailerSummary.php +35 -0
  256. package/server/src/routes.php +29 -0
  257. package/server/tests/CompactResourceSerializationTest.php +36 -0
  258. package/server/tests/ControllerFilterContractsTest.php +29 -0
  259. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +30 -13
  260. package/server/tests/Feature/Http/Api/TrailerControllerLifecycleTest.php +640 -0
  261. package/server/tests/Feature/Http/Internal/DeviceControllerContractsTest.php +20 -5
  262. package/server/tests/Feature/Http/Internal/LiveControllerEndpointsTest.php +19 -0
  263. package/server/tests/ModelAccessorContractsTest.php +10 -0
  264. package/server/tests/OrderFilterExecutionTest.php +5 -0
  265. package/server/tests/PublicApiControllerInputContractsTest.php +11 -0
  266. package/server/tests/PublicFleetOpsRestEndpointsTest.php +15 -3
  267. package/server/tests/ReportSchemaContractsTest.php +4 -0
  268. package/server/tests/TelematicsHardeningTest.php +4 -2
  269. package/server/tests/TrailerSupportCoverageTest.php +524 -0
  270. package/server/tests/Unit/Http/Resources/IndexVehicleResourceTest.php +35 -0
  271. package/server/tests/Unit/Models/EquipmentTest.php +22 -0
  272. package/server/tests/Unit/Models/ModelLookupFallbackSeamsTest.php +7 -3
  273. package/server/tests/Unit/Models/TrailerTest.php +204 -0
  274. package/server/tests/Unit/Support/Telematics/TelematicServiceTelemetryTest.php +93 -0
  275. package/server/tests/VehicleControllerHelperContractsTest.php +14 -0
  276. package/translations/ar-ae.yml +396 -0
  277. package/translations/bg-bg.yaml +424 -0
  278. package/translations/en-us.yaml +423 -0
  279. package/translations/es-pa.yaml +425 -0
  280. package/translations/fr-fr.yaml +424 -0
  281. package/translations/mn-mn.yaml +424 -0
  282. package/translations/pt-br.yaml +424 -0
  283. package/translations/ru-ru.yaml +424 -0
  284. package/translations/vi-vn.yaml +435 -0
@@ -29,6 +29,11 @@ export default class ManagementVehiclesIndexController extends Controller {
29
29
  'vehicle_make',
30
30
  'vehicle_model',
31
31
  'display_name',
32
+ 'trailer',
33
+ 'device',
34
+ 'has_trailer',
35
+ 'has_driver',
36
+ 'has_device',
32
37
  ];
33
38
  @tracked query = null;
34
39
  @tracked page = 1;
@@ -45,6 +50,11 @@ export default class ManagementVehiclesIndexController extends Controller {
45
50
  @tracked fleet;
46
51
  @tracked vendor;
47
52
  @tracked driver;
53
+ @tracked trailer;
54
+ @tracked device;
55
+ @tracked has_trailer;
56
+ @tracked has_driver;
57
+ @tracked has_device;
48
58
  @tracked display_name;
49
59
  @tracked table;
50
60
  @tracked layout = this.appCache.get('fleetops:vehicles:layout', 'table');
@@ -118,6 +128,17 @@ export default class ManagementVehiclesIndexController extends Controller {
118
128
  ];
119
129
  }
120
130
 
131
+ /**
132
+ * Yes/no choices for the "has trailer / driver / device" filters. `kind` picks the
133
+ * wording: assets are attached, drivers are assigned.
134
+ */
135
+ presenceOptions(kind) {
136
+ return [
137
+ { value: 'true', label: this.intl.t(`vehicle.filters.${kind}`) },
138
+ { value: 'false', label: this.intl.t(`vehicle.filters.not-${kind}`) },
139
+ ];
140
+ }
141
+
121
142
  /** columns */
122
143
  get columns() {
123
144
  return [
@@ -157,7 +178,8 @@ export default class ManagementVehiclesIndexController extends Controller {
157
178
  sortable: true,
158
179
  filterable: true,
159
180
  hidden: true,
160
- filterComponent: 'filter/string',
181
+ filterComponent: 'filter/multi-input',
182
+ filterComponentPlaceholder: this.intl.t('vehicle.placeholders.filter-internal-id'),
161
183
  filterParam: 'internal_id',
162
184
  },
163
185
  {
@@ -197,11 +219,63 @@ export default class ManagementVehiclesIndexController extends Controller {
197
219
  },
198
220
  resizable: true,
199
221
  filterable: true,
200
- filterComponent: 'filter/model',
201
- filterComponentPlaceholder: 'Select driver to filter by',
222
+ filterComponent: 'filter/multi-model',
223
+ filterComponentPlaceholder: this.intl.t('common.select-resource-filter-by', { resource: this.intl.t('resource.driver') }),
202
224
  filterParam: 'driver',
203
225
  model: 'driver',
204
226
  },
227
+ {
228
+ label: this.intl.t('resource.trailers'),
229
+ valuePath: 'trailers',
230
+ hidden: true,
231
+ filterable: true,
232
+ filterComponent: 'filter/multi-model',
233
+ filterComponentPlaceholder: this.intl.t('common.select-resource-filter-by', { resource: this.intl.t('resource.trailer') }),
234
+ filterParam: 'trailer',
235
+ model: 'trailer',
236
+ modelNamePath: 'displayName',
237
+ },
238
+ {
239
+ label: this.intl.t('resource.devices'),
240
+ valuePath: 'devices',
241
+ hidden: true,
242
+ filterable: true,
243
+ filterComponent: 'filter/multi-model',
244
+ filterComponentPlaceholder: this.intl.t('common.select-resource-filter-by', { resource: this.intl.t('resource.device') }),
245
+ filterParam: 'device',
246
+ model: 'device',
247
+ modelNamePath: 'displayName',
248
+ },
249
+ {
250
+ label: this.intl.t('vehicle.filters.has-trailer'),
251
+ valuePath: 'has_trailer',
252
+ hidden: true,
253
+ filterable: true,
254
+ filterComponent: 'filter/select',
255
+ filterComponentPlaceholder: this.intl.t('vehicle.filters.any'),
256
+ filterOptions: this.presenceOptions('attached'),
257
+ filterParam: 'has_trailer',
258
+ },
259
+ {
260
+ label: this.intl.t('vehicle.filters.has-driver'),
261
+ valuePath: 'has_driver',
262
+ hidden: true,
263
+ filterable: true,
264
+ filterComponent: 'filter/select',
265
+ filterComponentPlaceholder: this.intl.t('vehicle.filters.any'),
266
+ filterOptions: this.presenceOptions('assigned'),
267
+ filterParam: 'has_driver',
268
+ },
269
+ {
270
+ label: this.intl.t('vehicle.filters.has-device'),
271
+ valuePath: 'has_device',
272
+ hidden: true,
273
+ filterable: true,
274
+ filterComponent: 'filter/select',
275
+ filterComponentPlaceholder: this.intl.t('vehicle.filters.any'),
276
+ filterOptions: this.presenceOptions('attached'),
277
+ filterParam: 'has_device',
278
+ },
205
279
  {
206
280
  label: this.intl.t('column.id'),
207
281
  valuePath: 'public_id',
@@ -257,7 +331,7 @@ export default class ManagementVehiclesIndexController extends Controller {
257
331
  resizable: true,
258
332
  filterable: true,
259
333
  filterComponent: 'filter/model',
260
- filterComponentPlaceholder: this.intl.t('select-resource-filter-by', { resource: this.intl.t('resource.vendor') }),
334
+ filterComponentPlaceholder: this.intl.t('common.select-resource-filter-by', { resource: this.intl.t('resource.vendor') }),
261
335
  filterParam: 'vendor',
262
336
  model: 'vendor',
263
337
  },
@@ -1,12 +1,14 @@
1
1
  import Controller from '@ember/controller';
2
2
  import { inject as service } from '@ember/service';
3
3
  import { tracked } from '@glimmer/tracking';
4
+ import { action } from '@ember/object';
4
5
  import fleetOpsOptions from '../../../utils/fleet-ops-options';
5
6
 
6
7
  export default class ManagementVendorsIndexController extends Controller {
7
8
  @service vendorActions;
8
9
  @service tableContext;
9
10
  @service intl;
11
+ @service appCache;
10
12
 
11
13
  /** query params */
12
14
  @tracked queryParams = [
@@ -40,10 +42,35 @@ export default class ManagementVendorsIndexController extends Controller {
40
42
  @tracked email;
41
43
  @tracked country;
42
44
  @tracked table;
45
+ @tracked layout = this.appCache.get(this.layoutCacheKey, 'table');
46
+
47
+ /** cache key that remembers the chosen layout per listing */
48
+ get layoutCacheKey() {
49
+ return 'fleetops:vendors:layout';
50
+ }
43
51
 
44
52
  /** action buttons */
45
53
  get actionButtons() {
46
54
  return [
55
+ {
56
+ component: 'dropdown-button',
57
+ icon: 'display',
58
+ size: 'xs',
59
+ items: [
60
+ {
61
+ label: this.intl.t('common.table-view'),
62
+ icon: 'table-list',
63
+ onClick: () => this.setLayout('table'),
64
+ },
65
+ {
66
+ label: this.intl.t('common.grid-view'),
67
+ icon: 'grip',
68
+ onClick: () => this.setLayout('grid'),
69
+ },
70
+ ],
71
+ renderInPlace: true,
72
+ helpText: this.intl.t('common.change-layout'),
73
+ },
47
74
  {
48
75
  icon: 'refresh',
49
76
  onClick: this.vendorActions.refresh,
@@ -250,4 +277,9 @@ export default class ManagementVendorsIndexController extends Controller {
250
277
  },
251
278
  ];
252
279
  }
280
+
281
+ @action setLayout(layout) {
282
+ this.layout = layout;
283
+ this.appCache.set(this.layoutCacheKey, layout);
284
+ }
253
285
  }
@@ -157,7 +157,9 @@ export default class OperationsOrdersIndexController extends Controller {
157
157
  resizable: true,
158
158
  sortable: true,
159
159
  filterable: true,
160
- filterComponent: 'filter/string',
160
+ // Batch scanning: each scanned or pasted id becomes a chip and all of them match.
161
+ filterComponent: 'filter/multi-input',
162
+ filterComponentPlaceholder: this.intl.t('order.placeholders.filter-internal-id'),
161
163
  },
162
164
  {
163
165
  label: this.intl.t('column.payload'),
@@ -1,6 +1,8 @@
1
1
  import Controller from '@ember/controller';
2
+ import { inject as service } from '@ember/service';
2
3
 
3
4
  export default class SettingsCustomFieldsController extends Controller {
5
+ @service intl;
4
6
  get subjects() {
5
7
  return [
6
8
  {
@@ -15,6 +17,12 @@ export default class SettingsCustomFieldsController extends Controller {
15
17
  label: 'Vehicle',
16
18
  groups: [],
17
19
  },
20
+ {
21
+ model: 'trailer',
22
+ type: 'fleet-ops:trailer',
23
+ label: this.intl.t('resource.trailer'),
24
+ groups: [],
25
+ },
18
26
  {
19
27
  model: 'contact',
20
28
  type: 'fleet-ops:contact',
@@ -5,6 +5,7 @@ export default {
5
5
  const menuService = universe.getService('menu');
6
6
  const registryService = universe.getService('registry');
7
7
  const widgetService = universe.getService('widget');
8
+ const intlService = app.lookup('service:intl');
8
9
 
9
10
  // Register header navigation
10
11
  menuService.registerHeaderMenuItem('Fleet-Ops', 'console.fleet-ops', {
@@ -36,6 +37,12 @@ export default {
36
37
  icon: 'truck',
37
38
  route: 'console.fleet-ops.management.vehicles',
38
39
  },
40
+ {
41
+ title: intlService.t('menu.trailers'),
42
+ description: intlService.t('trailer.navigation-description'),
43
+ icon: 'trailer',
44
+ route: 'console.fleet-ops.management.trailers',
45
+ },
39
46
  {
40
47
  title: 'Fleets',
41
48
  description: 'Organise drivers and vehicles into operational fleets.',
@@ -0,0 +1,6 @@
1
+ import { helper } from '@ember/component/helper';
2
+ import { getPlaceholderImage } from '../utils/placeholder-images';
3
+
4
+ export default helper(function placeholderImage([type]) {
5
+ return getPlaceholderImage(type);
6
+ });
@@ -0,0 +1,10 @@
1
+ import { helper } from '@ember/component/helper';
2
+ import { resolveResourceImage } from '../utils/placeholder-images';
3
+
4
+ /**
5
+ * `{{resource-image @resource.photo_url "driver"}}` — the record's photo, or the styled
6
+ * placeholder for its resource type when the photo is missing or a legacy default.
7
+ */
8
+ export default helper(function resourceImage([url, type]) {
9
+ return resolveResourceImage(url, type);
10
+ });
@@ -0,0 +1,6 @@
1
+ import { helper } from '@ember/component/helper';
2
+ import getTrailerPlaceholderImage from '../utils/trailer-placeholder-image';
3
+
4
+ export default helper(function trailerPlaceholderImage() {
5
+ return getTrailerPlaceholderImage();
6
+ });
@@ -0,0 +1,2 @@
1
+ import Route from '@ember/routing/route';
2
+ export default class ManagementTrailersIndexDetailsConnectionsRoute extends Route {}
@@ -0,0 +1,2 @@
1
+ import Route from '@ember/routing/route';
2
+ export default class ManagementTrailersIndexDetailsDevicesRoute extends Route {}
@@ -0,0 +1,2 @@
1
+ import Route from '@ember/routing/route';
2
+ export default class ManagementTrailersIndexDetailsEquipmentRoute extends Route {}
@@ -0,0 +1,2 @@
1
+ import Route from '@ember/routing/route';
2
+ export default class ManagementTrailersIndexDetailsIndexRoute extends Route {}
@@ -0,0 +1,2 @@
1
+ import Route from '@ember/routing/route';
2
+ export default class ManagementTrailersIndexDetailsMaintenanceHistoryRoute extends Route {}
@@ -0,0 +1,2 @@
1
+ import Route from '@ember/routing/route';
2
+ export default class ManagementTrailersIndexDetailsPositionsRoute extends Route {}
@@ -0,0 +1,2 @@
1
+ import Route from '@ember/routing/route';
2
+ export default class ManagementTrailersIndexDetailsSchedulesRoute extends Route {}
@@ -0,0 +1,2 @@
1
+ import Route from '@ember/routing/route';
2
+ export default class ManagementTrailersIndexDetailsWorkOrdersRoute extends Route {}
@@ -0,0 +1,30 @@
1
+ import Route from '@ember/routing/route';
2
+ import { inject as service } from '@ember/service';
3
+ import { action } from '@ember/object';
4
+
5
+ export default class ManagementTrailersIndexDetailsRoute extends Route {
6
+ @service store;
7
+ @service notifications;
8
+ @service hostRouter;
9
+ @service abilities;
10
+ @service intl;
11
+
12
+ @action error(error) {
13
+ this.notifications.serverError(error);
14
+ if (typeof error?.message === 'string' && error.message.endsWith('not found')) {
15
+ return this.hostRouter.transitionTo('console.fleet-ops.management.trailers.index');
16
+ }
17
+ }
18
+
19
+ beforeModel() {
20
+ if (this.abilities.cannot('fleet-ops view trailer')) {
21
+ this.notifications.warning(this.intl.t('common.unauthorized-access'));
22
+ return this.hostRouter.transitionTo('console.fleet-ops.management.trailers.index');
23
+ }
24
+ }
25
+
26
+ model({ public_id }) {
27
+ // The details panel needs the towing state, so load the connection eagerly.
28
+ return this.store.queryRecord('trailer', { public_id, single: true, with: ['currentConnection.vehicle', 'category', 'vendor', 'warranty'] });
29
+ }
30
+ }
@@ -0,0 +1,23 @@
1
+ import Route from '@ember/routing/route';
2
+ import { inject as service } from '@ember/service';
3
+ import { action } from '@ember/object';
4
+ export default class ManagementTrailersIndexEditRoute extends Route {
5
+ @service store;
6
+ @service notifications;
7
+ @service hostRouter;
8
+ @service abilities;
9
+ @service intl;
10
+ @action error(error) {
11
+ this.notifications.serverError(error);
12
+ return this.hostRouter.transitionTo('console.fleet-ops.management.trailers.index');
13
+ }
14
+ beforeModel() {
15
+ if (this.abilities.cannot('fleet-ops update trailer')) {
16
+ this.notifications.warning(this.intl.t('common.unauthorized-access'));
17
+ return this.hostRouter.transitionTo('console.fleet-ops.management.trailers.index');
18
+ }
19
+ }
20
+ model({ public_id }) {
21
+ return this.store.findRecord('trailer', public_id);
22
+ }
23
+ }
@@ -0,0 +1,14 @@
1
+ import Route from '@ember/routing/route';
2
+ import { inject as service } from '@ember/service';
3
+ export default class ManagementTrailersIndexNewRoute extends Route {
4
+ @service notifications;
5
+ @service hostRouter;
6
+ @service abilities;
7
+ @service intl;
8
+ beforeModel() {
9
+ if (this.abilities.cannot('fleet-ops create trailer')) {
10
+ this.notifications.warning(this.intl.t('common.unauthorized-access'));
11
+ return this.hostRouter.transitionTo('console.fleet-ops.management.trailers.index');
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,42 @@
1
+ import Route from '@ember/routing/route';
2
+ import { inject as service } from '@ember/service';
3
+
4
+ export default class ManagementTrailersIndexRoute extends Route {
5
+ @service store;
6
+
7
+ queryParams = {
8
+ page: { refreshModel: true },
9
+ limit: { refreshModel: true },
10
+ sort: { refreshModel: true },
11
+ query: { refreshModel: true },
12
+ public_id: { refreshModel: true },
13
+ name: { refreshModel: true },
14
+ code: { refreshModel: true },
15
+ trailer_type: { refreshModel: true },
16
+ status: { refreshModel: true },
17
+ attachment_state: { refreshModel: true },
18
+ vehicle: { refreshModel: true },
19
+ connectivity_status: { refreshModel: true },
20
+ trailer_make: { refreshModel: true },
21
+ trailer_model: { refreshModel: true },
22
+ trailer_year: { refreshModel: true },
23
+ plate_number: { refreshModel: true },
24
+ vin: { refreshModel: true },
25
+ serial_number: { refreshModel: true },
26
+ vendor: { refreshModel: true },
27
+ ownership_type: { refreshModel: true },
28
+ refrigerated: { refreshModel: true },
29
+ device: { refreshModel: true },
30
+ last_online_at: { refreshModel: true },
31
+ created_at: { refreshModel: true },
32
+ updated_at: { refreshModel: true },
33
+ };
34
+
35
+ model(params) {
36
+ // Only forward meaningful filters; blank query params would otherwise be sent as
37
+ // empty strings and coerced into no-op or mismatching backend filters.
38
+ const query = Object.fromEntries(Object.entries(params).filter(([, value]) => value !== undefined && value !== null && value !== ''));
39
+
40
+ return this.store.query('trailer', query);
41
+ }
42
+ }
@@ -0,0 +1,2 @@
1
+ import Route from '@ember/routing/route';
2
+ export default class ManagementVehiclesIndexDetailsTrailersRoute extends Route {}
@@ -21,6 +21,11 @@ export default class ManagementVehiclesIndexRoute extends Route {
21
21
  fleet: { refreshModel: true },
22
22
  vendor: { refreshModel: true },
23
23
  driver: { refreshModel: true },
24
+ trailer: { refreshModel: true },
25
+ device: { refreshModel: true },
26
+ has_trailer: { refreshModel: true },
27
+ has_driver: { refreshModel: true },
28
+ has_device: { refreshModel: true },
24
29
  created_at: { refreshModel: true },
25
30
  updated_at: { refreshModel: true },
26
31
  'within[latitude]': { refreshModel: true, replace: true },
@@ -30,6 +35,7 @@ export default class ManagementVehiclesIndexRoute extends Route {
30
35
  };
31
36
 
32
37
  model(params) {
33
- return this.store.query('vehicle', { ...params });
38
+ // Current trailers back the card view's towing line; the internal API camelizes `with`.
39
+ return this.store.query('vehicle', { ...params, with: ['currentTrailers'] });
34
40
  }
35
41
  }
package/addon/routes.js CHANGED
@@ -86,6 +86,7 @@ export default buildRoutes(function () {
86
86
  this.route('positions');
87
87
  this.route('devices');
88
88
  this.route('equipment');
89
+ this.route('trailers');
89
90
  this.route('schedules');
90
91
  this.route('work-orders');
91
92
  this.route('maintenance-history');
@@ -94,6 +95,22 @@ export default buildRoutes(function () {
94
95
  this.route('edit', { path: '/edit/:public_id' });
95
96
  });
96
97
  });
98
+ this.route('trailers', function () {
99
+ this.route('index', { path: '/' }, function () {
100
+ this.route('new');
101
+ this.route('details', { path: '/:public_id' }, function () {
102
+ this.route('index', { path: '/' });
103
+ this.route('positions');
104
+ this.route('devices');
105
+ this.route('equipment');
106
+ this.route('connections');
107
+ this.route('schedules');
108
+ this.route('work-orders');
109
+ this.route('maintenance-history');
110
+ });
111
+ this.route('edit', { path: '/edit/:public_id' });
112
+ });
113
+ });
97
114
  this.route('places', function () {
98
115
  this.route('index', { path: '/' }, function () {
99
116
  this.route('new');
@@ -58,7 +58,7 @@ export default class DeviceActionsService extends ResourceActionService {
58
58
  {
59
59
  key: 'vehicle',
60
60
  id: 'vehicle',
61
- label: this.intl.t('resource.vehicle'),
61
+ label: this.intl.t('device.attachment.asset'),
62
62
  component: 'device/panel-tabs/vehicle',
63
63
  },
64
64
  {
@@ -112,26 +112,28 @@ export default class DeviceActionsService extends ResourceActionService {
112
112
  },
113
113
  };
114
114
 
115
- @action attachToVehicle(device, options = {}) {
115
+ @action attachToAsset(device, options = {}) {
116
116
  this.modalsManager.show('modals/attach-telematic-device', {
117
- title: this.intl.t('device.prompts.attach-to-vehicle-title'),
118
- acceptButtonText: this.intl.t('device.actions.attach-to-vehicle'),
117
+ title: this.intl.t('device.prompts.attach-to-asset-title'),
118
+ acceptButtonText: this.intl.t('device.actions.attach-to-asset'),
119
119
  acceptButtonIcon: 'link',
120
120
  device,
121
- selectedVehicle: null,
121
+ selectedAssetType: null,
122
+ selectedAsset: null,
122
123
  confirm: async (modal) => {
123
- const selectedVehicle = modal.getOption('selectedVehicle');
124
+ const selectedAssetType = modal.getOption('selectedAssetType');
125
+ const selectedAsset = modal.getOption('selectedAsset');
124
126
 
125
- if (!selectedVehicle) {
126
- return this.notifications.warning(this.intl.t('device.prompts.select-vehicle-warning'));
127
+ if (!selectedAssetType || !selectedAsset) {
128
+ return this.notifications.warning(this.intl.t('device.prompts.select-asset-warning'));
127
129
  }
128
130
 
129
131
  modal.startLoading();
130
132
 
131
133
  try {
132
- await this.fetch.post(`devices/${device.id}/attach`, { vehicle: selectedVehicle.id });
134
+ await this.fetch.post(`devices/${device.id}/attach`, { attachable_type: selectedAssetType.value, attachable: selectedAsset.id });
133
135
  await device.reload?.();
134
- this.notifications.success(this.intl.t('device.prompts.attach-to-vehicle-success'));
136
+ this.notifications.success(this.intl.t('device.prompts.attach-to-asset-success'));
135
137
  modal.done();
136
138
  this.refresh();
137
139
  } catch (error) {
@@ -143,24 +145,28 @@ export default class DeviceActionsService extends ResourceActionService {
143
145
  });
144
146
  }
145
147
 
146
- @action detachFromVehicle(device, options = {}) {
148
+ @action attachToVehicle(device, options = {}) {
149
+ return this.attachToAsset(device, options);
150
+ }
151
+
152
+ @action detachFromAsset(device, options = {}) {
147
153
  const deviceName = device.displayName ?? device.name ?? this.intl.t('resource.device');
148
- const vehicleName = device.attached_to_name ?? device.attachable?.display_name ?? device.attachable?.name;
154
+ const assetName = device.attached_to_name ?? device.attachable?.display_name ?? device.attachable?.name;
149
155
 
150
- if (!device.attachable_uuid && !vehicleName) {
156
+ if (!device.attachable_uuid && !assetName) {
151
157
  return this.notifications.warning(this.intl.t('device.prompts.not-attached-warning', { deviceName }));
152
158
  }
153
159
 
154
160
  this.modalsManager.confirm({
155
- title: this.intl.t('device.prompts.detach-from-vehicle-title', { deviceName }),
156
- body: this.intl.t('device.prompts.detach-from-vehicle-body', { deviceName, vehicleName: vehicleName ?? this.intl.t('resource.vehicle') }),
161
+ title: this.intl.t('device.prompts.detach-from-asset-title', { deviceName }),
162
+ body: this.intl.t('device.prompts.detach-from-asset-body', { deviceName, assetName: assetName ?? this.intl.t('device.attachment.asset') }),
157
163
  confirm: async (modal) => {
158
164
  modal.startLoading();
159
165
 
160
166
  try {
161
167
  await this.fetch.post(`devices/${device.id}/detach`);
162
168
  await device.reload?.();
163
- this.notifications.success(this.intl.t('device.prompts.detach-from-vehicle-success'));
169
+ this.notifications.success(this.intl.t('device.prompts.detach-from-asset-success'));
164
170
  modal.done();
165
171
  this.refresh();
166
172
  } catch (error) {
@@ -171,4 +177,8 @@ export default class DeviceActionsService extends ResourceActionService {
171
177
  ...options,
172
178
  });
173
179
  }
180
+
181
+ @action detachFromVehicle(device, options = {}) {
182
+ return this.detachFromAsset(device, options);
183
+ }
174
184
  }