@fleetbase/fleetops-engine 0.6.54 → 0.6.55

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 (127) hide show
  1. package/addon/components/admin/routing-settings.hbs +2 -1
  2. package/addon/components/cell/attached-vehicle.hbs +10 -0
  3. package/addon/components/cell/attached-vehicle.js +58 -0
  4. package/addon/components/cell/device-identity.hbs +24 -0
  5. package/addon/components/cell/device-identity.js +129 -0
  6. package/addon/components/cell/driver-identity.hbs +33 -0
  7. package/addon/components/cell/driver-identity.js +143 -0
  8. package/addon/components/cell/equipment-identity.hbs +5 -0
  9. package/addon/components/cell/equipment-identity.js +48 -0
  10. package/addon/components/cell/part-identity.hbs +5 -0
  11. package/addon/components/cell/part-identity.js +63 -0
  12. package/addon/components/cell/telematic-device.hbs +38 -0
  13. package/addon/components/cell/telematic-device.js +44 -0
  14. package/addon/components/cell/telematic-provider.hbs +33 -16
  15. package/addon/components/cell/telematic-provider.js +50 -8
  16. package/addon/components/cell/vehicle-identity.hbs +33 -0
  17. package/addon/components/cell/vehicle-identity.js +138 -0
  18. package/addon/components/device/form.hbs +2 -2
  19. package/addon/components/device/form.js +7 -0
  20. package/addon/components/device/panel-header.hbs +45 -20
  21. package/addon/components/device/panel-header.js +49 -1
  22. package/addon/components/device/panel-tabs/events.hbs +30 -0
  23. package/addon/components/device/panel-tabs/events.js +128 -0
  24. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  25. package/addon/components/device/panel-tabs/sensors.js +93 -0
  26. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  27. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  28. package/addon/components/equipment/form.js +8 -1
  29. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  30. package/addon/components/layout/fleet-ops-sidebar.js +8 -0
  31. package/addon/components/map/drawer/device-event-listing.js +2 -0
  32. package/addon/components/map/drawer/driver-listing.js +2 -0
  33. package/addon/components/map/drawer/place-listing.js +2 -0
  34. package/addon/components/map/drawer/position-listing.hbs +11 -6
  35. package/addon/components/map/drawer/position-listing.js +17 -4
  36. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  37. package/addon/components/sensor/panel-header.hbs +47 -15
  38. package/addon/components/sensor/panel-header.js +59 -1
  39. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  40. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  41. package/addon/components/table/cell/dropdown.hbs +28 -0
  42. package/addon/components/table/cell/dropdown.js +91 -0
  43. package/addon/components/vendor/panel-header.hbs +82 -0
  44. package/addon/components/vendor/panel-header.js +48 -0
  45. package/addon/controllers/connectivity/devices/index/details/vehicle.js +49 -1
  46. package/addon/controllers/connectivity/devices/index.js +144 -129
  47. package/addon/controllers/connectivity/events/details.js +3 -0
  48. package/addon/controllers/connectivity/events/index.js +187 -11
  49. package/addon/controllers/connectivity/sensors/index.js +156 -14
  50. package/addon/controllers/connectivity/telematics/details/devices.js +126 -134
  51. package/addon/controllers/maintenance/equipment/index.js +1 -2
  52. package/addon/controllers/maintenance/parts/index.js +1 -2
  53. package/addon/controllers/management/drivers/index.js +82 -42
  54. package/addon/controllers/management/fuel-reports/index.js +63 -4
  55. package/addon/controllers/management/issues/index.js +63 -4
  56. package/addon/controllers/management/vehicles/index.js +33 -6
  57. package/addon/routes/connectivity/devices/index/details/vehicle.js +24 -2
  58. package/addon/routes/connectivity/devices/index.js +7 -0
  59. package/addon/routes/connectivity/events/details.js +3 -0
  60. package/addon/routes/connectivity/events/index.js +6 -2
  61. package/addon/routes/connectivity/sensors/index.js +7 -2
  62. package/addon/routes/connectivity/telematics/details/devices.js +2 -0
  63. package/addon/services/device-actions.js +30 -0
  64. package/addon/services/device-event-actions.js +1 -1
  65. package/addon/services/equipment-actions.js +1 -0
  66. package/addon/services/sensor-actions.js +3 -0
  67. package/addon/services/service-rate-actions.js +39 -1
  68. package/addon/services/vehicle-actions.js +24 -12
  69. package/addon/services/vendor-actions.js +3 -0
  70. package/addon/styles/fleetops-engine.css +8 -0
  71. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +64 -1
  72. package/addon/templates/management/vendors/index/details.hbs +2 -2
  73. package/addon/templates/settings/notifications.hbs +2 -1
  74. package/addon/templates/settings/routing.hbs +1 -0
  75. package/addon/utils/device-table-columns.js +288 -0
  76. package/addon/utils/identity-cell-resource.js +20 -0
  77. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  78. package/addon/utils/trackable-option.js +93 -0
  79. package/app/components/cell/attached-vehicle.js +1 -0
  80. package/app/components/cell/device-identity.js +1 -0
  81. package/app/components/cell/driver-identity.js +1 -0
  82. package/app/components/cell/equipment-identity.js +1 -0
  83. package/app/components/cell/part-identity.js +1 -0
  84. package/app/components/cell/telematic-device.js +1 -0
  85. package/app/components/cell/vehicle-identity.js +1 -0
  86. package/app/components/device/panel-tabs/events.hbs +30 -0
  87. package/app/components/device/panel-tabs/events.js +128 -0
  88. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  89. package/app/components/device/panel-tabs/sensors.js +93 -0
  90. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  91. package/app/components/device/panel-tabs/vehicle.js +90 -0
  92. package/app/components/table/cell/dropdown/action-item.js +1 -0
  93. package/app/components/table/cell/dropdown.js +1 -0
  94. package/app/components/vendor/panel-header.js +1 -0
  95. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  96. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  97. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  98. package/app/utils/map-drawer-dropdown-position.js +1 -0
  99. package/app/utils/trackable-option.js +1 -0
  100. package/composer.json +1 -1
  101. package/extension.json +1 -1
  102. package/package.json +4 -4
  103. package/server/src/Console/Commands/SyncTelematics.php +4 -4
  104. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -1
  105. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +1 -1
  106. package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
  107. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  108. package/server/src/Http/Filter/DeviceEventFilter.php +21 -0
  109. package/server/src/Http/Filter/DeviceFilter.php +24 -1
  110. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  111. package/server/src/Models/Device.php +2 -0
  112. package/server/src/Models/DeviceEvent.php +81 -0
  113. package/server/src/Models/Equipment.php +55 -0
  114. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +1 -0
  115. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  116. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  117. package/server/src/Support/Telematics/Providers/SafeeProvider.php +26 -2
  118. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  119. package/server/src/routes.php +0 -1
  120. package/server/tests/DeviceFilterTest.php +9 -0
  121. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  122. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  123. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  124. package/server/tests/TelematicsHardeningTest.php +238 -7
  125. package/addon/controllers/connectivity/events/index/details.js +0 -3
  126. package/addon/routes/connectivity/events/index/details.js +0 -3
  127. /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
@@ -1,21 +1,84 @@
1
1
  import Controller from '@ember/controller';
2
+ import { action, get } from '@ember/object';
2
3
  import { inject as service } from '@ember/service';
3
4
  import { tracked } from '@glimmer/tracking';
4
5
  import fleetOpsOptions from '../../../utils/fleet-ops-options';
5
6
 
7
+ function sensorTelematic(sensor) {
8
+ const telematic = get(sensor, 'telematic');
9
+
10
+ if (telematic) {
11
+ return telematic;
12
+ }
13
+
14
+ const id = get(sensor, 'telematic_uuid');
15
+ const name = get(sensor, 'telematic_name');
16
+ const provider = get(sensor, 'provider');
17
+
18
+ if (!id && !name && !provider) {
19
+ return null;
20
+ }
21
+
22
+ return {
23
+ id,
24
+ name: name ?? provider,
25
+ provider,
26
+ provider_descriptor: get(sensor, 'provider_descriptor'),
27
+ };
28
+ }
29
+
30
+ function sensorDevice(sensor) {
31
+ const device = get(sensor, 'device');
32
+
33
+ if (device) {
34
+ return device;
35
+ }
36
+
37
+ const id = get(sensor, 'device_uuid');
38
+ const deviceId = get(sensor, 'device_id') ?? get(sensor, 'provider_device_id') ?? get(sensor, 'ident');
39
+ const name = get(sensor, 'device_name') ?? deviceId;
40
+ const imei = get(sensor, 'device_imei') ?? get(sensor, 'imei') ?? get(sensor, 'ident');
41
+
42
+ if (!id && !name && !imei && !deviceId) {
43
+ return null;
44
+ }
45
+
46
+ return {
47
+ id,
48
+ displayName: name,
49
+ name,
50
+ imei,
51
+ device_id: deviceId,
52
+ ident: get(sensor, 'ident'),
53
+ serial_number: get(sensor, 'device_serial_number'),
54
+ connection_status: get(sensor, 'device_connection_status'),
55
+ status: get(sensor, 'device_status'),
56
+ photo_url: get(sensor, 'device_photo_url') ?? get(sensor, 'photo_url'),
57
+ };
58
+ }
59
+
6
60
  export default class ConnectivitySensorsIndexController extends Controller {
7
61
  @service sensorActions;
8
62
  @service deviceActions;
9
63
  @service telematicActions;
10
64
  @service intl;
65
+ @service store;
11
66
 
12
67
  /** query params */
13
- @tracked queryParams = ['name', 'page', 'limit', 'sort', 'query', 'public_id', 'created_at', 'updated_at'];
68
+ @tracked queryParams = ['page', 'limit', 'sort', 'query', 'telematic', 'device', 'type', 'status', 'serial_number', 'imei', 'last_reading_at', 'created_at', 'updated_at'];
14
69
  @tracked page = 1;
15
70
  @tracked limit;
16
71
  @tracked sort = '-created_at';
17
- @tracked public_id;
18
- @tracked name;
72
+ @tracked query;
73
+ @tracked telematic;
74
+ @tracked device;
75
+ @tracked type;
76
+ @tracked status;
77
+ @tracked serial_number;
78
+ @tracked imei;
79
+ @tracked last_reading_at;
80
+ @tracked created_at;
81
+ @tracked updated_at;
19
82
 
20
83
  /** action buttons */
21
84
  @tracked actionButtons = [
@@ -67,14 +130,16 @@ export default class ConnectivitySensorsIndexController extends Controller {
67
130
  resizable: true,
68
131
  sortable: true,
69
132
  filterable: true,
70
- filterParam: 'name',
133
+ filterParam: 'query',
71
134
  filterComponent: 'filter/string',
72
135
  },
73
136
  {
74
137
  label: 'Telematic',
75
138
  valuePath: 'telematic.provider',
76
- cellComponent: 'table/cell/anchor',
77
- action: this.telematicActions.transition.view,
139
+ cellComponent: 'cell/telematic-provider',
140
+ resourcePath: sensorTelematic,
141
+ compact: true,
142
+ action: this.openTelematic,
78
143
  permission: 'fleet-ops view telematic',
79
144
  resizable: true,
80
145
  sortable: true,
@@ -87,8 +152,10 @@ export default class ConnectivitySensorsIndexController extends Controller {
87
152
  {
88
153
  label: 'Device',
89
154
  valuePath: 'device.displayName',
90
- cellComponent: 'table/cell/anchor',
91
- action: this.deviceActions.transition.view,
155
+ cellComponent: 'cell/device-identity',
156
+ resourcePath: sensorDevice,
157
+ showStatus: false,
158
+ action: this.openDevice,
92
159
  permission: 'fleet-ops view device',
93
160
  resizable: true,
94
161
  sortable: true,
@@ -97,10 +164,14 @@ export default class ConnectivitySensorsIndexController extends Controller {
97
164
  filterComponentPlaceholder: 'Select device',
98
165
  filterParam: 'device',
99
166
  model: 'device',
167
+ modelNamePath: 'displayName',
168
+ query: this.telematic ? { telematic_uuid: this.telematic } : undefined,
100
169
  },
101
170
  {
102
171
  label: 'Type',
103
172
  valuePath: 'type',
173
+ cellComponent: 'table/cell/base',
174
+ humanize: true,
104
175
  resizable: true,
105
176
  sortable: true,
106
177
  filterable: true,
@@ -109,13 +180,16 @@ export default class ConnectivitySensorsIndexController extends Controller {
109
180
  filterOptions: fleetOpsOptions('sensorTypes'),
110
181
  },
111
182
  {
112
- label: 'Serial Number',
113
- valuePath: 'serial_number',
183
+ label: 'Last Value',
184
+ valuePath: 'last_value',
185
+ resizable: true,
186
+ sortable: true,
187
+ },
188
+ {
189
+ label: 'Unit',
190
+ valuePath: 'unit',
114
191
  resizable: true,
115
192
  sortable: true,
116
- filterable: true,
117
- filterParam: 'serial_number',
118
- filterComponent: 'filter/string',
119
193
  },
120
194
  {
121
195
  label: this.intl.t('column.status'),
@@ -127,6 +201,41 @@ export default class ConnectivitySensorsIndexController extends Controller {
127
201
  filterComponent: 'filter/multi-option',
128
202
  filterOptions: fleetOpsOptions('sensorStatuses'),
129
203
  },
204
+ {
205
+ label: 'Threshold',
206
+ valuePath: 'threshold_status',
207
+ cellComponent: 'table/cell/status',
208
+ resizable: true,
209
+ sortable: true,
210
+ },
211
+ {
212
+ label: 'Serial Number',
213
+ valuePath: 'serial_number',
214
+ resizable: true,
215
+ sortable: true,
216
+ filterable: true,
217
+ filterParam: 'serial_number',
218
+ filterComponent: 'filter/string',
219
+ },
220
+ {
221
+ label: 'IMEI',
222
+ valuePath: 'imei',
223
+ resizable: true,
224
+ sortable: true,
225
+ filterable: true,
226
+ filterParam: 'imei',
227
+ filterComponent: 'filter/string',
228
+ },
229
+ {
230
+ label: 'Last Reading',
231
+ valuePath: 'lastReadingAt',
232
+ sortParam: 'last_reading_at',
233
+ resizable: true,
234
+ sortable: true,
235
+ filterable: true,
236
+ filterParam: 'last_reading_at',
237
+ filterComponent: 'filter/date',
238
+ },
130
239
  {
131
240
  label: this.intl.t('column.created-at'),
132
241
  valuePath: 'createdAt',
@@ -134,6 +243,7 @@ export default class ConnectivitySensorsIndexController extends Controller {
134
243
  resizable: true,
135
244
  sortable: true,
136
245
  filterable: true,
246
+ filterParam: 'created_at',
137
247
  filterComponent: 'filter/date',
138
248
  },
139
249
  {
@@ -144,9 +254,9 @@ export default class ConnectivitySensorsIndexController extends Controller {
144
254
  sortable: true,
145
255
  hidden: true,
146
256
  filterable: true,
257
+ filterParam: 'updated_at',
147
258
  filterComponent: 'filter/date',
148
259
  },
149
-
150
260
  {
151
261
  label: '',
152
262
  cellComponent: 'table/cell/dropdown',
@@ -184,4 +294,36 @@ export default class ConnectivitySensorsIndexController extends Controller {
184
294
  searchable: false,
185
295
  },
186
296
  ];
297
+
298
+ async resolveDevice(device) {
299
+ if (!device?.id) {
300
+ return null;
301
+ }
302
+
303
+ const cachedDevice = this.store.peekRecord('device', device.id);
304
+
305
+ if (cachedDevice) {
306
+ return cachedDevice;
307
+ }
308
+
309
+ try {
310
+ return await this.store.findRecord('device', device.id);
311
+ } catch (_) {
312
+ return device;
313
+ }
314
+ }
315
+
316
+ @action async openDevice(device) {
317
+ const resolvedDevice = await this.resolveDevice(device);
318
+
319
+ if (resolvedDevice?.id) {
320
+ return this.deviceActions.panel.view(resolvedDevice);
321
+ }
322
+ }
323
+
324
+ @action openTelematic(telematic) {
325
+ if (telematic?.id) {
326
+ return this.telematicActions.transition.view(telematic);
327
+ }
328
+ }
187
329
  }
@@ -3,24 +3,36 @@ import { action } from '@ember/object';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { tracked } from '@glimmer/tracking';
5
5
  import { task } from 'ember-concurrency';
6
-
7
- const connectionStatusOptions = [
8
- { label: 'Online', value: 'online' },
9
- { label: 'Recently Offline', value: 'recently_offline' },
10
- { label: 'Offline', value: 'offline' },
11
- { label: 'Long Offline', value: 'long_offline' },
12
- { label: 'Never Connected', value: 'never_connected' },
13
- ];
6
+ import buildDeviceTableColumns from '../../../../utils/device-table-columns';
7
+ import fleetOpsOptions from '../../../../utils/fleet-ops-options';
14
8
 
15
9
  export default class ConnectivityTelematicsDetailsDevicesController extends Controller {
16
10
  @service deviceActions;
17
11
  @service fetch;
18
12
  @service hostRouter;
19
13
  @service intl;
14
+ @service mapManager;
20
15
  @service modalsManager;
21
16
  @service notifications;
22
-
23
- @tracked queryParams = ['page', 'limit', 'sort', 'query', 'status', 'provider', 'attachment_state', 'vehicle', 'connection_status', 'device_id', 'last_online_at', 'updated_at'];
17
+ @service store;
18
+ @service vehicleActions;
19
+
20
+ @tracked queryParams = [
21
+ 'page',
22
+ 'limit',
23
+ 'sort',
24
+ 'query',
25
+ 'status',
26
+ 'provider',
27
+ 'attachment_state',
28
+ 'vehicle',
29
+ 'connection_status',
30
+ 'device_id',
31
+ 'type',
32
+ 'serial_number',
33
+ 'last_online_at',
34
+ 'updated_at',
35
+ ];
24
36
  @tracked telematic;
25
37
  @tracked page = 1;
26
38
  @tracked limit;
@@ -32,6 +44,8 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
32
44
  @tracked vehicle;
33
45
  @tracked connection_status;
34
46
  @tracked device_id;
47
+ @tracked type;
48
+ @tracked serial_number;
35
49
  @tracked last_online_at;
36
50
  @tracked updated_at;
37
51
 
@@ -57,7 +71,17 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
57
71
 
58
72
  get hasActiveFilters() {
59
73
  return Boolean(
60
- this.query || this.status || this.provider || this.attachment_state || this.vehicle || this.connection_status || this.device_id || this.last_online_at || this.updated_at
74
+ this.query ||
75
+ this.status ||
76
+ this.provider ||
77
+ this.attachment_state ||
78
+ this.vehicle ||
79
+ this.connection_status ||
80
+ this.device_id ||
81
+ this.type ||
82
+ this.serial_number ||
83
+ this.last_online_at ||
84
+ this.updated_at
61
85
  );
62
86
  }
63
87
 
@@ -194,130 +218,16 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
194
218
 
195
219
  @tracked bulkActions = [];
196
220
 
221
+ get deviceTypeOptions() {
222
+ return fleetOpsOptions('deviceTypes');
223
+ }
224
+
225
+ get deviceStatusOptions() {
226
+ return fleetOpsOptions('deviceStatuses');
227
+ }
228
+
197
229
  get columns() {
198
- return [
199
- {
200
- sticky: true,
201
- label: this.intl.t('column.name'),
202
- valuePath: 'displayName',
203
- cellComponent: 'table/cell/anchor',
204
- action: this.deviceActions.transition.view,
205
- permission: 'fleet-ops view device',
206
- resizable: true,
207
- sortable: true,
208
- filterable: true,
209
- filterParam: 'query',
210
- filterComponent: 'filter/string',
211
- },
212
- {
213
- label: 'Provider ID',
214
- valuePath: 'device_id',
215
- resizable: true,
216
- sortable: true,
217
- filterable: true,
218
- filterParam: 'device_id',
219
- filterComponent: 'filter/string',
220
- },
221
- {
222
- label: 'Vehicle',
223
- valuePath: 'attached_to_name',
224
- resizable: true,
225
- sortable: true,
226
- filterable: true,
227
- filterParam: 'vehicle',
228
- filterComponent: 'filter/model',
229
- filterComponentPlaceholder: 'Select vehicle',
230
- model: 'vehicle',
231
- modelNamePath: 'displayName',
232
- },
233
- {
234
- label: 'Connection',
235
- valuePath: 'connection_status',
236
- cellComponent: 'table/cell/status',
237
- resizable: true,
238
- sortable: true,
239
- filterable: true,
240
- filterParam: 'connection_status',
241
- filterComponent: 'filter/multi-option',
242
- filterOptions: connectionStatusOptions,
243
- filterOptionLabel: 'label',
244
- filterOptionValue: 'value',
245
- },
246
- {
247
- label: 'Last Seen',
248
- valuePath: 'lastOnlineAt',
249
- sortParam: 'last_online_at',
250
- resizable: true,
251
- sortable: true,
252
- filterable: true,
253
- filterParam: 'last_online_at',
254
- filterComponent: 'filter/date',
255
- },
256
- {
257
- label: 'Attachment',
258
- valuePath: 'attachable_uuid',
259
- hidden: true,
260
- resizable: true,
261
- sortable: false,
262
- filterable: true,
263
- filterParam: 'attachment_state',
264
- filterComponent: 'filter/select',
265
- filterOptions: [
266
- { label: 'Attached', value: 'attached' },
267
- { label: 'Unattached', value: 'unattached' },
268
- ],
269
- filterOptionLabel: 'label',
270
- filterOptionValue: 'value',
271
- },
272
- {
273
- label: this.intl.t('column.updated-at'),
274
- valuePath: 'updatedAt',
275
- sortParam: 'updated_at',
276
- resizable: true,
277
- sortable: true,
278
- hidden: true,
279
- filterable: true,
280
- filterComponent: 'filter/date',
281
- },
282
- {
283
- label: '',
284
- cellComponent: 'table/cell/dropdown',
285
- ddButtonText: false,
286
- ddButtonIcon: 'ellipsis-h',
287
- ddButtonIconPrefix: 'fas',
288
- ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.device') }),
289
- cellClassNames: 'overflow-visible align-middle',
290
- wrapperClass: 'flex items-center justify-end mx-2',
291
- sticky: 'right',
292
- width: 60,
293
- actions: [
294
- {
295
- label: this.intl.t('common.view-resource', { resource: this.intl.t('resource.device') }),
296
- fn: this.deviceActions.transition.view,
297
- permission: 'fleet-ops view device',
298
- },
299
- {
300
- label: this.intl.t('common.edit-resource', { resource: this.intl.t('resource.device') }),
301
- fn: this.deviceActions.transition.edit,
302
- permission: 'fleet-ops update device',
303
- },
304
- {
305
- label: 'Attach or change vehicle',
306
- fn: this.openAttachDeviceModal,
307
- permission: 'fleet-ops update device',
308
- },
309
- {
310
- label: 'Review recent events',
311
- fn: this.openDeviceEvents,
312
- permission: 'fleet-ops view device-event',
313
- },
314
- ],
315
- sortable: false,
316
- filterable: false,
317
- resizable: false,
318
- searchable: false,
319
- },
320
- ];
230
+ return buildDeviceTableColumns(this, { showProvider: false, deviceActionMode: 'panel', showDeviceStatus: false });
321
231
  }
322
232
 
323
233
  @action refresh() {
@@ -336,6 +246,8 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
336
246
  this.vehicle = null;
337
247
  this.connection_status = null;
338
248
  this.device_id = null;
249
+ this.type = null;
250
+ this.serial_number = null;
339
251
  this.last_online_at = null;
340
252
  this.updated_at = null;
341
253
  this.page = 1;
@@ -358,6 +270,86 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
358
270
  });
359
271
  }
360
272
 
273
+ @action hasAttachedVehicle(device) {
274
+ return Boolean(device?.attachable_uuid && this.isVehicleAttachment(device));
275
+ }
276
+
277
+ @action async viewAttachedVehicle(device) {
278
+ const vehicle = await this.resolveAttachedVehicle(device);
279
+
280
+ if (!vehicle) {
281
+ return;
282
+ }
283
+
284
+ return this.vehicleActions.panel.view(vehicle);
285
+ }
286
+
287
+ @action async locateAttachedVehicle(device) {
288
+ const vehicle = await this.resolveAttachedVehicle(device);
289
+
290
+ if (!vehicle) {
291
+ return;
292
+ }
293
+
294
+ await this.transitionToLiveMap();
295
+ await this.mapManager.waitForMap({ timeoutMs: 8000 });
296
+
297
+ this.mapManager.focusResource(vehicle, 16, {
298
+ paddingBottomRight: [300, 200],
299
+ moveend: () => {
300
+ this.vehicleActions.panel.view(vehicle, { closeOnTransition: true });
301
+ },
302
+ });
303
+ }
304
+
305
+ async transitionToLiveMap() {
306
+ try {
307
+ await this.hostRouter.transitionTo('console.fleet-ops.operations.orders.index', { queryParams: { layout: 'map' } });
308
+ } catch (_) {
309
+ // Keep locate actions usable even if the current transition is already in-flight.
310
+ }
311
+ }
312
+
313
+ isVehicleAttachment(device) {
314
+ const attachableType = `${device?.attachable_type ?? ''}`.toLowerCase();
315
+
316
+ return !attachableType || attachableType.includes('vehicle');
317
+ }
318
+
319
+ async resolveAttachedVehicle(device) {
320
+ if (!device?.attachable_uuid) {
321
+ return null;
322
+ }
323
+
324
+ if (!this.isVehicleAttachment(device)) {
325
+ return null;
326
+ }
327
+
328
+ const attachable = device.attachable;
329
+
330
+ if (attachable && typeof attachable.then !== 'function') {
331
+ return attachable;
332
+ }
333
+
334
+ if (attachable && typeof attachable.then === 'function') {
335
+ return await attachable;
336
+ }
337
+
338
+ const cachedVehicle = this.store.peekRecord('vehicle', device.attachable_uuid);
339
+
340
+ if (cachedVehicle) {
341
+ return cachedVehicle;
342
+ }
343
+
344
+ try {
345
+ return await this.store.findRecord('vehicle', device.attachable_uuid);
346
+ } catch (error) {
347
+ this.notifications.serverError(error);
348
+ }
349
+
350
+ return null;
351
+ }
352
+
361
353
  @action openDeviceEvents(device) {
362
354
  return this.hostRouter.transitionTo('console.fleet-ops.connectivity.telematics.details.events', this.telematic, {
363
355
  queryParams: { device_uuid: device.id },
@@ -61,8 +61,7 @@ export default class MaintenanceEquipmentIndexController extends Controller {
61
61
  {
62
62
  label: this.intl.t('column.name'),
63
63
  valuePath: 'name',
64
- cellComponent: 'table/cell/anchor',
65
- cellClassNames: 'uppercase',
64
+ cellComponent: 'cell/equipment-identity',
66
65
  action: this.equipmentActions.transition.view,
67
66
  permission: 'fleet-ops view equipment',
68
67
  resizable: true,
@@ -61,8 +61,7 @@ export default class MaintenancePartsIndexController extends Controller {
61
61
  {
62
62
  label: this.intl.t('column.name'),
63
63
  valuePath: 'name',
64
- cellComponent: 'table/cell/anchor',
65
- cellClassNames: 'uppercase',
64
+ cellComponent: 'cell/part-identity',
66
65
  action: this.partActions.transition.view,
67
66
  permission: 'fleet-ops view part',
68
67
  resizable: true,