@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
package/RELEASE.md CHANGED
@@ -1,13 +1,15 @@
1
- > v0.6.64 ~ "Order responses no longer fail when a vehicle is assigned"
1
+ > v0.6.65 ~ "First-class trailer operations"
2
2
 
3
3
  ---
4
- ## Bug Fixes
5
- - **Orders with an assigned vehicle serialize again when expansions are requested.** The Vehicle and Fleet resources read the request's `with` list and loaded it onto the model they wrap. That is correct on their own endpoints, where the controller has already allowlisted the list, but both resources are also rendered nested inside Order and Driver responses. There the same request carries the parent's expansions, so `GET /v1/orders?with=payload` on an order with a vehicle assigned failed during serialisation with `RelationNotFoundException: Call to undefined relationship [payload] on model Vehicle`. Navigator order fetches and the console scheduler both hit this. Nested resources now keep only the relations the wrapped model actually defines; direct endpoint behaviour is unchanged.
4
+ ## What's New
5
+ - **Trailers become a first-class Fleet-Ops resource.** Create and manage trailers independently, attach them to vehicles, connect equipment and telematics devices, and review operational, maintenance, location, and towing details from dedicated Trailer screens.
6
+ - **Vehicle and equipment relationships are easier to understand.** Vehicle details expose attached trailers and equipment, while Trailer details show the current towing vehicle and installed equipment.
7
+ - **Trailer APIs support complete integrations.** Internal and public endpoints cover trailer lifecycle, filtering, towing connections, equipment, devices, telemetry, imports, and exports.
6
8
 
7
9
  ---
8
10
  ## Testing
9
- - Added resource-level regressions for parent expansions reaching a nested Vehicle or Fleet resource, for string and empty `with` forms, and for resources wrapping plain data.
10
- - Added end-to-end checks resolving a real vehicle and a real fleet against an order-shaped request.
11
+ - Added frontend, backend, API-contract, attachment, telematics, spatial-data, permission, and serialization coverage for first-class trailers.
12
+ - Expanded the Fleetbase Postman collection and documentation alongside the Fleet-Ops implementation.
11
13
 
12
14
  ---
13
15
  ## Need help?
@@ -0,0 +1,65 @@
1
+ <div class="fleetops-attachment-list {{@wrapperClass}}" ...attributes>
2
+ <div class="fleetops-attachment-list-header">
3
+ <div class="min-w-0">
4
+ <h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{@title}}</h3>
5
+ {{#if @description}}
6
+ <p class="text-xs text-gray-500 dark:text-gray-400">{{@description}}</p>
7
+ {{/if}}
8
+ </div>
9
+ <div class="flex flex-row items-center space-x-2 shrink-0">
10
+ {{yield to="actions"}}
11
+ {{#if @actionText}}
12
+ <Button
13
+ @icon={{or @actionIcon "plus"}}
14
+ @text={{@actionText}}
15
+ @size="xs"
16
+ @type={{or @actionType "primary"}}
17
+ @onClick={{@onAction}}
18
+ @permission={{@actionPermission}}
19
+ @disabled={{@actionDisabled}}
20
+ />
21
+ {{/if}}
22
+ </div>
23
+ </div>
24
+
25
+ {{#if @isLoading}}
26
+ <div class="fleetops-attachment-list-loading">
27
+ <Spinner />
28
+ </div>
29
+ {{else if @items.length}}
30
+ <div class="fleetops-attachment-list-items" role="list">
31
+ {{#each @items as |item index|}}
32
+ <div class="fleetops-attachment-list-item" role="listitem">
33
+ <div class="fleetops-attachment-list-item-content">
34
+ {{yield item index to="item"}}
35
+ </div>
36
+ {{#if (has-block "itemActions")}}
37
+ <div class="fleetops-attachment-list-item-actions">
38
+ {{yield item index to="itemActions"}}
39
+ </div>
40
+ {{/if}}
41
+ </div>
42
+ {{/each}}
43
+ </div>
44
+ {{else}}
45
+ <div class="fleetops-attachment-list-empty">
46
+ <FaIcon @icon={{or @emptyIcon "box-open"}} @size="2x" class="text-gray-400 dark:text-gray-500 mb-2" />
47
+ <h3 class="text-base font-semibold text-gray-700 dark:text-gray-300">{{@emptyTitle}}</h3>
48
+ {{#if @emptyDescription}}
49
+ <p class="text-sm text-gray-500 dark:text-gray-400">{{@emptyDescription}}</p>
50
+ {{/if}}
51
+ {{#if (and @actionText (not @hideEmptyAction))}}
52
+ <Button
53
+ @icon={{or @actionIcon "plus"}}
54
+ @text={{@actionText}}
55
+ @size="xs"
56
+ @type={{or @actionType "primary"}}
57
+ @onClick={{@onAction}}
58
+ @permission={{@actionPermission}}
59
+ @disabled={{@actionDisabled}}
60
+ class="mt-3"
61
+ />
62
+ {{/if}}
63
+ </div>
64
+ {{/if}}
65
+ </div>
@@ -0,0 +1,8 @@
1
+ import Component from '@glimmer/component';
2
+
3
+ /**
4
+ * Shared layout for "things attached to an asset" tabs (devices, equipment,
5
+ * trailers, towing history) so every attachment tab reads the same way: a
6
+ * header with a description and primary action, uniform rows, one empty state.
7
+ */
8
+ export default class AttachmentListComponent extends Component {}
@@ -4,7 +4,7 @@
4
4
  {{else}}
5
5
  <div class="flex items-center gap-2 py-1 text-xs text-gray-400 dark:text-gray-500">
6
6
  <FaIcon @icon="link-slash" />
7
- <span>Unattached</span>
7
+ <span>{{t "trailer.attachment.detached"}}</span>
8
8
  </div>
9
9
  {{/if}}
10
10
  </div>
@@ -31,13 +31,13 @@ export default class CellAttachedVehicleComponent extends Component {
31
31
  }
32
32
 
33
33
  get hasVehicle() {
34
- return Boolean(this.device?.attachable_uuid && this.isVehicleAttachment);
34
+ return Boolean(this.device?.attachable_uuid && this.isSupportedAttachment);
35
35
  }
36
36
 
37
- get isVehicleAttachment() {
37
+ get isSupportedAttachment() {
38
38
  const attachableType = `${this.device?.attachable_type ?? ''}`.toLowerCase();
39
39
 
40
- return !attachableType || attachableType.includes('vehicle');
40
+ return !attachableType || attachableType.includes('vehicle') || attachableType.includes('trailer');
41
41
  }
42
42
 
43
43
  @action onClick(_vehicle, event) {
@@ -1,6 +1,6 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { action, get } from '@ember/object';
3
- import config from 'ember-get-config';
3
+ import { getPlaceholderImage, resolveResourceImage } from '../../utils/placeholder-images';
4
4
  import { resolveIdentityCellResource } from '../../utils/identity-cell-resource';
5
5
 
6
6
  const DEFAULT_STATUS_TONES = {
@@ -34,11 +34,11 @@ export default class CellDriverIdentityComponent extends Component {
34
34
  }
35
35
 
36
36
  get mediaUrl() {
37
- return get(this.resource, 'photo_url');
37
+ return resolveResourceImage(get(this.resource, 'photo_url'), 'driver');
38
38
  }
39
39
 
40
40
  get fallbackImage() {
41
- return config?.defaultValues?.driverImage;
41
+ return getPlaceholderImage('driver');
42
42
  }
43
43
 
44
44
  get hasCompactStatusDot() {
@@ -97,7 +97,7 @@ export default class CellDriverIdentityComponent extends Component {
97
97
  ...(this.args.column ?? {}),
98
98
  labelPath: 'name',
99
99
  mediaPath: 'photo_url',
100
- fallbackImage: config?.defaultValues?.driverImage,
100
+ fallbackImage: getPlaceholderImage('driver'),
101
101
  statusPath: 'status',
102
102
  onlinePath: 'online',
103
103
  showStatusBadge: this.args.column?.showStatusBadge ?? true,
@@ -1,7 +1,7 @@
1
1
  {{#let (or @row.driver this.driver) as |driver|}}
2
2
  {{#if driver}}
3
3
  <div class="relative flex items-center" ...attributes>
4
- <img src={{driver.photo_url}} alt={{driver.name}} {{fallback-img-src (config "defaultValues.driverImage")}} class="w-5 h-5 mr-2 rounded-md" />
4
+ <img src={{resource-image driver.photo_url "driver"}} alt={{driver.name}} {{fallback-img-src (placeholder-image "driver")}} class="w-5 h-5 mr-2 rounded-md" />
5
5
  <a href="javascript:;" class="relative flex flex-row items-center" disabled={{and @column.permission (cannot @column.permission)}} {{on "click" (fn this.onClick driver)}}>
6
6
  <span>{{get-default-value driver.name}}</span>
7
7
  {{#if @row.vehicle_assigned}}
@@ -0,0 +1,9 @@
1
+ {{#if this.label}}
2
+ {{#if this.isBadge}}
3
+ <Badge @status={{this.value}} @text={{this.label}} @icon={{this.icon}} @hideStatusDot={{unless this.icon false true}} ...attributes />
4
+ {{else}}
5
+ <span class={{@column.className}} ...attributes>{{this.label}}</span>
6
+ {{/if}}
7
+ {{else}}
8
+ <span class="text-gray-400 dark:text-gray-500" ...attributes>{{this.emptyText}}</span>
9
+ {{/if}}
@@ -0,0 +1,57 @@
1
+ import Component from '@glimmer/component';
2
+ import { inject as service } from '@ember/service';
3
+ import { get } from '@ember/object';
4
+
5
+ /**
6
+ * Table cell that renders a stable enum value (`dry_van`, `never_connected`, ...)
7
+ * through its localized label. Configure the column with:
8
+ *
9
+ * translationPrefix: 'trailer.types' // required, joined with the raw value
10
+ * badge: true // optional, render as a status badge
11
+ * badgeIconPath / badgeIcons // optional badge icon per value
12
+ */
13
+ export default class CellTranslatedValueComponent extends Component {
14
+ @service intl;
15
+
16
+ get value() {
17
+ const row = this.args.row;
18
+ const valuePath = this.args.column?.valuePath;
19
+
20
+ if (this.args.value !== undefined && this.args.value !== null) {
21
+ return this.args.value;
22
+ }
23
+
24
+ return valuePath ? get(row, valuePath) : null;
25
+ }
26
+
27
+ get label() {
28
+ const value = this.value;
29
+ const prefix = this.args.column?.translationPrefix;
30
+
31
+ if (value === null || value === undefined || value === '') {
32
+ return null;
33
+ }
34
+
35
+ if (!prefix) {
36
+ return String(value);
37
+ }
38
+
39
+ const key = `${prefix}.${value}`;
40
+
41
+ return this.intl.exists(key) ? this.intl.t(key) : String(value);
42
+ }
43
+
44
+ get isBadge() {
45
+ return Boolean(this.args.column?.badge);
46
+ }
47
+
48
+ get icon() {
49
+ const icons = this.args.column?.badgeIcons;
50
+
51
+ return icons && this.value ? icons[this.value] : undefined;
52
+ }
53
+
54
+ get emptyText() {
55
+ return this.args.column?.emptyText ?? '-';
56
+ }
57
+ }
@@ -0,0 +1,58 @@
1
+ <Layout::Resource::Card class="fleetops-resource-card contact-card" ...attributes as |Card|>
2
+ <Card.header class={{@headerClass}}>
3
+ <RegistryYield @registry="fleet-ops:component:contact:card:header:start" as |RegistryComponent|>
4
+ <RegistryComponent @resource={{@resource}} />
5
+ </RegistryYield>
6
+ <div class="fleetops-card-title">
7
+ <div class="font-semibold flex-1 truncate">{{n-a @resource.name}}</div>
8
+ </div>
9
+ <div class="text-gray-400 dark:text-gray-500 text-sm w-full truncate">{{n-a @resource.title @resource.internal_id @resource.public_id}}</div>
10
+ {{#if (has-block "header")}}
11
+ {{yield to="header"}}
12
+ {{/if}}
13
+ <RegistryYield @registry="fleet-ops:component:contact:card:header:end" as |RegistryComponent|>
14
+ <RegistryComponent @resource={{@resource}} />
15
+ </RegistryYield>
16
+ </Card.header>
17
+ <Card.body class="{{@bodyClass}} h-40 overflow-hidden">
18
+ <Image src={{resource-image @resource.photo_url this.placeholderType}} @fallbackSrc={{(placeholder-image this.placeholderType)}} alt={{@resource.name}} class="fleetops-card-image fleetops-card-image--cover" />
19
+ {{#if (has-block "body")}}
20
+ {{yield to="body"}}
21
+ {{/if}}
22
+ </Card.body>
23
+ <Card.footer class={{@footerClass}}>
24
+ <RegistryYield @registry="fleet-ops:component:contact:card:footer:start" as |RegistryComponent|>
25
+ <RegistryComponent @resource={{@resource}} />
26
+ </RegistryYield>
27
+ <div class="flex flex-row items-center justify-between w-full">
28
+ <Badge @status={{or @resource.status @resource.type "default"}} @size="xs" />
29
+ <div class="flex flex-row items-center space-x-1">
30
+ <Button @icon="eye" @size="xs" @helpText={{t "common.view"}} @onClick={{fn this.resourceActions.transition.view @resource}} @permission="fleet-ops view contact" />
31
+ <Button @icon="pencil" @size="xs" @helpText={{t "common.edit"}} @onClick={{fn this.resourceActions.transition.edit @resource}} @permission="fleet-ops update contact" />
32
+ <Button @icon="trash" @size="xs" @type="danger" @helpText={{t "common.delete"}} @onClick={{fn this.resourceActions.delete @resource}} @permission="fleet-ops delete contact" />
33
+ </div>
34
+ </div>
35
+ <RegistryYield @registry="fleet-ops:component:contact:card:footer:end" as |RegistryComponent|>
36
+ <RegistryComponent @resource={{@resource}} />
37
+ </RegistryYield>
38
+ {{#if (has-block "footer")}}
39
+ {{yield to="footer"}}
40
+ {{/if}}
41
+ <div class="fleetops-card-meta-list">
42
+ <div class="fleetops-card-meta">
43
+ <FaIcon @icon="envelope" @prefix="fas" />
44
+ <span class="fleetops-card-meta-value">{{#if @resource.email}}<a href="mailto:{{@resource.email}}" class="fleetops-card-meta-link">{{@resource.email}}</a>{{else}}-{{/if}}</span>
45
+ </div>
46
+ <div class="fleetops-card-meta">
47
+ <FaIcon @icon="phone" @prefix="fas" />
48
+ <span class="fleetops-card-meta-value">{{#if @resource.phone}}<a href="tel:{{@resource.phone}}" class="fleetops-card-meta-link">{{@resource.phone}}</a>{{else}}-{{/if}}</span>
49
+ </div>
50
+ <div class="fleetops-card-meta">
51
+ <FaIcon @icon="location-dot" @prefix="fas" />
52
+ <span class="fleetops-card-meta-value">{{n-a @resource.address @resource.place.address}}</span>
53
+ </div>
54
+ <div class="fleetops-card-meta">{{t "common.last-modified"}}: {{@resource.updatedAt}}</div>
55
+ </div>
56
+ </Card.footer>
57
+ </Layout::Resource::Card>
58
+ {{yield}}
@@ -0,0 +1,19 @@
1
+ import Component from '@glimmer/component';
2
+ import { inject as service } from '@ember/service';
3
+
4
+ /**
5
+ * Card for contacts and customers. Customers are contacts driven by the customer
6
+ * actions service, so the actions service can be passed in as `@resourceActions`.
7
+ */
8
+ export default class ContactCardComponent extends Component {
9
+ @service contactActions;
10
+
11
+ get resourceActions() {
12
+ return this.args.resourceActions ?? this.contactActions;
13
+ }
14
+
15
+ /** Customers get their own silhouette; plain contacts the generic one. */
16
+ get placeholderType() {
17
+ return this.args.placeholderType ?? 'contact';
18
+ }
19
+ }
@@ -2,7 +2,7 @@
2
2
  <ContentPanel @title={{t "contact.fields.contact-details"}} @open={{true}} @wrapperClass="bordered-top">
3
3
  <div class="flex flex-row space-x-3 mb-3">
4
4
  <div class="flex items-center justify-start rounded-full">
5
- <Image src={{@resource.photo_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{@resource.name}} height="48" width="48" class="h-14 w-14 rounded-full shadow-sm" />
5
+ <Image src={{resource-image @resource.photo_url "contact"}} @fallbackSrc={{(placeholder-image "contact")}} alt={{@resource.name}} height="48" width="48" class="h-14 w-14 rounded-full shadow-sm" />
6
6
  <Attach::Tooltip @class="clean" @animation="scale" @placement="top">
7
7
  <InputInfo @text={{t "common.upload-new-photo"}} />
8
8
  </Attach::Tooltip>
@@ -2,7 +2,7 @@
2
2
  <div class="flex flex-1 flex-row items-start justify-between">
3
3
  <div class="flex flex-row space-x-3">
4
4
  <div class="flex items-start justify-start rounded-full">
5
- <Image src={{@resource.photo_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{@resource.name}} height="48" width="48" class="h-14 w-14 rounded-full shadow-sm" />
5
+ <Image src={{resource-image @resource.photo_url "contact"}} @fallbackSrc={{(placeholder-image "contact")}} alt={{@resource.name}} height="48" width="48" class="h-14 w-14 rounded-full shadow-sm" />
6
6
  <Attach::Tooltip @class="clean" @animation="scale" @placement="top">
7
7
  <InputInfo @text={{@resource.name}} />
8
8
  </Attach::Tooltip>
@@ -3,8 +3,8 @@
3
3
  <div class="flex flex-row space-x-3 mb-3">
4
4
  <div class="flex items-center justify-start rounded-full">
5
5
  <Image
6
- src={{@resource.photo_url}}
7
- @fallbackSrc={{config "defaultValues.contactImage"}}
6
+ src={{resource-image @resource.photo_url "customer"}}
7
+ @fallbackSrc={{(placeholder-image "customer")}}
8
8
  alt={{@resource.name}}
9
9
  height="48"
10
10
  width="48"
@@ -1,28 +1,26 @@
1
- <div class="fleetops-device-manager" ...attributes>
2
- <div class="flex flex-row items-center justify-between mb-4 px-3 pt-2">
3
- <div></div>
4
- <div>
5
- <Button @icon="plus" @text={{t "device.actions.attach-device"}} @onClick={{this.addDevice}} @disabled={{not @resource}} />
6
- </div>
7
- </div>
8
- <div>
9
- {{#each this.devices as |device|}}
10
- <div class="flex flex-row justify-between px-3 py-4 border-t border-gray-200 dark:border-gray-700">
11
- <Device::Pill @device={{device}} />
12
- <div>
13
- <Button @type="danger" @icon="times" @text={{t "device.actions.detach-from-vehicle"}} @size="xs" @onClick={{fn this.removeDevice device}} />
14
- </div>
15
- </div>
16
- {{else}}
17
- <div class="empty-state bg-white dark:bg-gray-800 rounded-lg shadow-sm p-12 text-center">
18
- <FaIcon @icon="hard-drive" @size="2x" class="text-gray-400 mb-2" />
19
- <h3 class="text-base font-semibold text-gray-700 dark:text-gray-300">
20
- {{t "device.prompts.no-devices-attached-title"}}
21
- </h3>
22
- <p class="text-sm text-gray-500 dark:text-gray-400">
23
- {{t "device.prompts.no-devices-attached-body" resourceType=(get-model-name @resource)}}
24
- </p>
25
- </div>
26
- {{/each}}
27
- </div>
28
- </div>
1
+ <AttachmentList
2
+ @title={{t "resource.devices"}}
3
+ @description={{t "device.prompts.manager-description" resourceType=(get-model-name @resource)}}
4
+ @actionText={{t "device.actions.attach-device"}}
5
+ @actionIcon="plus"
6
+ @onAction={{this.addDevice}}
7
+ @actionDisabled={{not @resource}}
8
+ @isLoading={{this.loadDevices.isRunning}}
9
+ @items={{this.devices}}
10
+ @emptyIcon="hard-drive"
11
+ @emptyTitle={{t "device.prompts.no-devices-attached-title"}}
12
+ @emptyDescription={{t "device.prompts.no-devices-attached-body" resourceType=(get-model-name @resource)}}
13
+ class="fleetops-device-manager"
14
+ ...attributes
15
+ >
16
+ <:item as |device|>
17
+ <Device::Pill @device={{device}} @onClick={{fn this.viewDevice device}} />
18
+ {{#if device.status}}
19
+ <Badge @status={{device.status}} @size="xs" class="fleetops-attachment-list-item-badge" />
20
+ {{/if}}
21
+ </:item>
22
+ <:itemActions as |device|>
23
+ <Button @icon="eye" @size="xs" @helpText={{t "common.view"}} @onClick={{fn this.viewDevice device}} />
24
+ <Button @type="danger" @icon="unlink" @size="xs" @helpText={{t "device.attachment.detach"}} @onClick={{fn this.removeDevice device}} />
25
+ </:itemActions>
26
+ </AttachmentList>
@@ -5,6 +5,8 @@ import { inject as service } from '@ember/service';
5
5
  import { debug } from '@ember/debug';
6
6
  import { task } from 'ember-concurrency';
7
7
  import getModelName from '@fleetbase/ember-core/utils/get-model-name';
8
+ import { pluralize } from 'ember-inflector';
9
+ import { dasherize } from '@ember/string';
8
10
 
9
11
  export default class DeviceManagerComponent extends Component {
10
12
  @service store;
@@ -12,6 +14,7 @@ export default class DeviceManagerComponent extends Component {
12
14
  @service modalsManager;
13
15
  @service notifications;
14
16
  @service intl;
17
+ @service deviceActions;
15
18
  @tracked devices = [];
16
19
 
17
20
  get resourceName() {
@@ -28,6 +31,21 @@ export default class DeviceManagerComponent extends Component {
28
31
  );
29
32
  }
30
33
 
34
+ /**
35
+ * The internal attach/detach endpoints are namespaced by the attachable resource
36
+ * (`vehicles/{id}/attach-device`, `trailers/{id}/attach-device`). Derive the prefix
37
+ * from the resource's model name unless an explicit `@endpoint` is given.
38
+ */
39
+ get endpoint() {
40
+ if (this.args.endpoint) {
41
+ return this.args.endpoint;
42
+ }
43
+
44
+ const modelName = getModelName(this.args.resource);
45
+
46
+ return modelName ? dasherize(pluralize(modelName)) : 'vehicles';
47
+ }
48
+
31
49
  constructor() {
32
50
  super(...arguments);
33
51
  this.loadDevices.perform();
@@ -45,7 +63,7 @@ export default class DeviceManagerComponent extends Component {
45
63
  modal.startLoading();
46
64
 
47
65
  try {
48
- await this.fetch.post(`vehicles/${this.args.resource.id}/attach-device`, { device: selectedDevice.id });
66
+ await this.fetch.post(`${this.endpoint}/${this.args.resource.id}/attach-device`, { device: selectedDevice.id });
49
67
  await this.loadDevices.perform();
50
68
  this.notifications.success(this.intl.t('device.prompts.attach-device-success'));
51
69
  modal.done();
@@ -57,6 +75,14 @@ export default class DeviceManagerComponent extends Component {
57
75
  });
58
76
  }
59
77
 
78
+ @action viewDevice(device) {
79
+ if (this.deviceActions.panel?.view) {
80
+ return this.deviceActions.panel.view(device);
81
+ }
82
+
83
+ return this.deviceActions.transition.view(device);
84
+ }
85
+
60
86
  @action removeDevice(device) {
61
87
  const deviceName = device.displayName ?? device.name ?? device.device_id ?? this.intl.t('resource.device');
62
88
 
@@ -67,7 +93,7 @@ export default class DeviceManagerComponent extends Component {
67
93
  modal.startLoading();
68
94
 
69
95
  try {
70
- await this.fetch.post(`vehicles/${this.args.resource.id}/detach-device`, { device: device.id });
96
+ await this.fetch.post(`${this.endpoint}/${this.args.resource.id}/detach-device`, { device: device.id });
71
97
  await this.loadDevices.perform();
72
98
  this.notifications.success(this.intl.t('device.prompts.detach-from-resource-success', { deviceName, resourceName: this.resourceName }));
73
99
  modal.done();
@@ -1,21 +1,21 @@
1
1
  <div class="flex flex-col gap-3 p-3" ...attributes>
2
2
  <div class="flex items-start justify-between gap-3">
3
3
  <div class="min-w-0">
4
- <h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Vehicle Attachment</h2>
5
- <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">Fleet context for telemetry from this device.</p>
4
+ <h2 class="text-base font-bold text-gray-900 dark:text-gray-100">{{t "device.attachment.title"}}</h2>
5
+ <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">{{t "device.attachment.description"}}</p>
6
6
  </div>
7
7
  <div class="flex flex-wrap items-center justify-end gap-2">
8
8
  {{#if this.hasVehicle}}
9
9
  {{#if this.canOpenVehicle}}
10
- <Button @icon="eye" @text="Open Vehicle" @size="xs" @onClick={{this.openVehicle}} />
10
+ <Button @icon="eye" @text={{t "device.attachment.open"}} @size="xs" @onClick={{this.openVehicle}} />
11
11
  {{/if}}
12
12
  {{#if this.canLocateVehicle}}
13
- <Button @icon="location-dot" @text="Locate" @size="xs" @onClick={{this.locateVehicle}} />
13
+ <Button @icon="location-dot" @text={{t "device.attachment.locate"}} @size="xs" @onClick={{this.locateVehicle}} />
14
14
  {{/if}}
15
- <Button @icon="shuffle" @text="Change Vehicle" @size="xs" @onClick={{this.attachToVehicle}} />
16
- <Button @icon="unlink" @text="Detach" @size="xs" @type="danger" @onClick={{this.detachFromVehicle}} />
15
+ <Button @icon="shuffle" @text={{t "device.attachment.change"}} @size="xs" @onClick={{this.attachToVehicle}} />
16
+ <Button @icon="unlink" @text={{t "device.attachment.detach"}} @size="xs" @type="danger" @onClick={{this.detachFromVehicle}} />
17
17
  {{else}}
18
- <Button @icon="link" @text="Attach Vehicle" @size="xs" @onClick={{this.attachToVehicle}} />
18
+ <Button @icon="link" @text={{t "device.actions.attach-to-asset"}} @size="xs" @onClick={{this.attachToVehicle}} />
19
19
  {{/if}}
20
20
  </div>
21
21
  </div>
@@ -39,15 +39,15 @@
39
39
  <div class="mt-1 text-xs text-gray-500 dark:text-gray-300">{{n-a this.vehicleSubtitle}}</div>
40
40
  <div class="mt-3 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3">
41
41
  <div>
42
- <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Driver</div>
42
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">{{t "resource.driver"}}</div>
43
43
  <div class="truncate text-gray-800 dark:text-gray-100">{{n-a this.vehicleDriverName}}</div>
44
44
  </div>
45
45
  <div>
46
- <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Attached Device</div>
46
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">{{t "device.attachment.attached-device"}}</div>
47
47
  <div class="truncate text-gray-800 dark:text-gray-100">{{n-a this.device.displayName this.device.name}}</div>
48
48
  </div>
49
49
  <div>
50
- <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Device Last Seen</div>
50
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">{{t "device.attachment.last-seen"}}</div>
51
51
  <div class="truncate text-gray-800 dark:text-gray-100">{{n-a (format-date-fns this.device.last_online_at "dd MMM yyyy, HH:mm")}}</div>
52
52
  </div>
53
53
  </div>
@@ -58,9 +58,9 @@
58
58
  <Telematic::TabEmptyState
59
59
  @tone="warning"
60
60
  @icon="link-slash"
61
- @title="No vehicle attached"
62
- @message="Attach this device to a vehicle so telemetry has fleet context."
63
- @primaryText="Attach Vehicle"
61
+ @title={{t "device.attachment.none-title"}}
62
+ @message={{t "device.attachment.none-message"}}
63
+ @primaryText={{t "device.actions.attach-to-asset"}}
64
64
  @primaryIcon="link"
65
65
  @primaryAction={{this.attachToVehicle}}
66
66
  />
@@ -7,6 +7,7 @@ export default class DevicePanelTabsVehicleComponent extends Component {
7
7
  @service hostRouter;
8
8
  @service mapManager;
9
9
  @service vehicleActions;
10
+ @service trailerActions;
10
11
 
11
12
  get device() {
12
13
  return this.args.resource ?? this.args.model;
@@ -16,6 +17,10 @@ export default class DevicePanelTabsVehicleComponent extends Component {
16
17
  return this.device?.attachable;
17
18
  }
18
19
 
20
+ get isTrailer() {
21
+ return `${this.device?.attachable_type ?? ''}`.toLowerCase().includes('trailer');
22
+ }
23
+
19
24
  get vehicleName() {
20
25
  return this.device?.attached_to_name ?? this.vehicle?.displayName ?? this.vehicle?.display_name ?? this.vehicle?.name;
21
26
  }
@@ -58,6 +63,9 @@ export default class DevicePanelTabsVehicleComponent extends Component {
58
63
 
59
64
  @action openVehicle() {
60
65
  if (this.vehicle?.id) {
66
+ if (this.isTrailer) {
67
+ return this.trailerActions.transition.view(this.vehicle);
68
+ }
61
69
  return this.vehicleActions.panel?.view
62
70
  ? this.vehicleActions.panel.view(this.vehicle)
63
71
  : this.hostRouter.transitionTo('console.fleet-ops.management.vehicles.index.details', this.vehicle);