@fleetbase/fleetops-engine 0.6.52 → 0.6.54

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 (151) hide show
  1. package/addon/components/device/details.hbs +106 -135
  2. package/addon/components/device/details.js +174 -1
  3. package/addon/components/device/panel-header.hbs +2 -2
  4. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.hbs +201 -0
  5. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +654 -0
  6. package/addon/components/layout/fleet-ops-sidebar.hbs +19 -233
  7. package/addon/components/layout/fleet-ops-sidebar.js +364 -395
  8. package/addon/components/maintenance-schedule/form.js +1 -0
  9. package/addon/components/order/form/details.hbs +1 -43
  10. package/addon/components/order/form/details.js +18 -53
  11. package/addon/components/order/form/orchestrator-constraints.hbs +40 -0
  12. package/addon/components/order/form/orchestrator-constraints.js +61 -0
  13. package/addon/components/order/form/payload.js +50 -2
  14. package/addon/components/order/form/route.js +15 -0
  15. package/addon/components/order/form/service-rate.hbs +62 -11
  16. package/addon/components/order/form/service-rate.js +124 -2
  17. package/addon/components/order/form.hbs +2 -0
  18. package/addon/components/telematic/details.hbs +7 -1
  19. package/addon/components/telematic/form.js +16 -8
  20. package/addon/components/work-order/details.hbs +5 -1
  21. package/addon/components/work-order/form.hbs +26 -4
  22. package/addon/components/work-order/form.js +5 -1
  23. package/addon/controllers/connectivity/devices/index/details/events.js +244 -1
  24. package/addon/controllers/connectivity/devices/index/details/sensors.js +199 -0
  25. package/addon/controllers/connectivity/devices/index/details/vehicle.js +74 -0
  26. package/addon/controllers/connectivity/devices/index/details.js +12 -0
  27. package/addon/controllers/connectivity/devices/index.js +4 -1
  28. package/addon/controllers/connectivity/telematics/details/attachments.js +220 -54
  29. package/addon/controllers/connectivity/telematics/details/devices.js +90 -41
  30. package/addon/controllers/connectivity/telematics/details/events.js +39 -11
  31. package/addon/controllers/connectivity/telematics/details/sensors.js +39 -19
  32. package/addon/controllers/maintenance/index.js +100 -0
  33. package/addon/controllers/maintenance/work-orders/index.js +18 -1
  34. package/addon/controllers/management/fuel-transactions/index.js +1 -1
  35. package/addon/controllers/management/index.js +88 -0
  36. package/addon/controllers/settings/index.js +142 -0
  37. package/addon/extension.js +21 -0
  38. package/addon/routes/analytics/index.js +3 -0
  39. package/addon/routes/connectivity/devices/index/details/events.js +37 -1
  40. package/addon/routes/connectivity/devices/index/details/sensors.js +35 -0
  41. package/addon/routes/connectivity/devices/index/details/vehicle.js +7 -0
  42. package/addon/routes/connectivity/devices/index.js +3 -0
  43. package/addon/routes/connectivity/telematics/details/attachments.js +72 -9
  44. package/addon/routes/connectivity/telematics/details/devices.js +4 -0
  45. package/addon/routes/connectivity/telematics/details/sensors.js +1 -0
  46. package/addon/routes/connectivity/telematics/details.js +1 -1
  47. package/addon/routes/connectivity/telematics/edit.js +1 -1
  48. package/addon/routes/maintenance/index.js +3 -0
  49. package/addon/routes/maintenance/work-orders/index.js +3 -0
  50. package/addon/routes/settings/index.js +3 -0
  51. package/addon/routes.js +2 -0
  52. package/addon/services/order-creation.js +54 -1
  53. package/addon/services/order-validation.js +3 -2
  54. package/addon/services/service-rate-actions.js +1 -0
  55. package/addon/styles/fleetops-engine.css +1821 -6
  56. package/addon/templates/analytics/index.hbs +14 -0
  57. package/addon/templates/analytics/reports/index.hbs +4 -4
  58. package/addon/templates/connectivity/devices/index/details/events.hbs +85 -1
  59. package/addon/templates/connectivity/devices/index/details/sensors.hbs +73 -0
  60. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +84 -0
  61. package/addon/templates/connectivity/telematics/details/attachments.hbs +198 -121
  62. package/addon/templates/connectivity/telematics/details/devices.hbs +4 -1
  63. package/addon/templates/connectivity/telematics/details/events.hbs +4 -1
  64. package/addon/templates/connectivity/telematics/details/sensors.hbs +3 -0
  65. package/addon/templates/maintenance/index.hbs +113 -0
  66. package/addon/templates/management/fuel-transactions/index/details.hbs +1 -1
  67. package/addon/templates/management/index.hbs +109 -73
  68. package/addon/templates/management/vehicles/index/edit.hbs +1 -1
  69. package/addon/templates/operations/orders/index.hbs +1 -1
  70. package/addon/templates/operations/service-rates/index.hbs +1 -1
  71. package/addon/templates/settings/index.hbs +83 -0
  72. package/addon/templates/settings/routing.hbs +2 -2
  73. package/addon/utils/fleet-ops-options.js +38 -0
  74. package/app/components/order/form/orchestrator-constraints.js +1 -0
  75. package/app/controllers/connectivity/devices/index/details/sensors.js +1 -0
  76. package/app/controllers/connectivity/devices/index/details/vehicle.js +1 -0
  77. package/app/controllers/maintenance/index.js +1 -0
  78. package/app/controllers/management/index.js +1 -0
  79. package/app/controllers/settings/index.js +1 -0
  80. package/app/routes/analytics/index.js +1 -0
  81. package/app/routes/connectivity/devices/index/details/sensors.js +1 -0
  82. package/app/routes/connectivity/devices/index/details/vehicle.js +1 -0
  83. package/app/routes/maintenance/index.js +1 -0
  84. package/app/routes/settings/index.js +1 -0
  85. package/app/templates/analytics/index.js +1 -0
  86. package/app/templates/connectivity/devices/index/details/sensors.js +1 -0
  87. package/app/templates/connectivity/devices/index/details/vehicle.js +1 -0
  88. package/app/templates/maintenance/index.js +1 -0
  89. package/app/templates/management/index.js +1 -0
  90. package/app/templates/settings/index.js +1 -0
  91. package/composer.json +1 -1
  92. package/extension.json +1 -1
  93. package/package.json +3 -3
  94. package/server/migrations/2025_08_28_054930_create_work_orders_table.php +1 -0
  95. package/server/migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php +30 -0
  96. package/server/migrations/2026_06_18_000001_add_category_to_work_orders_table.php +32 -0
  97. package/server/seeders/Testing/MaintenanceSeeder.php +5 -4
  98. package/server/src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php +1 -0
  99. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +1 -0
  100. package/server/src/Console/Commands/SyncTelematics.php +89 -0
  101. package/server/src/Exceptions/TelematicProviderException.php +13 -2
  102. package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -21
  103. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +112 -11
  104. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +16 -5
  105. package/server/src/Http/Controllers/Internal/v1/FleetController.php +9 -0
  106. package/server/src/Http/Controllers/Internal/v1/HubController.php +300 -0
  107. package/server/src/Http/Controllers/Internal/v1/LiveController.php +177 -0
  108. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +1 -0
  109. package/server/src/Http/Controllers/Internal/v1/OrderController.php +4 -32
  110. package/server/src/Http/Controllers/Internal/v1/SearchController.php +506 -0
  111. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +4 -4
  112. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +1 -1
  113. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +94 -10
  114. package/server/src/Http/Filter/DeviceEventFilter.php +44 -0
  115. package/server/src/Http/Filter/DeviceFilter.php +131 -0
  116. package/server/src/Http/Filter/DriverFilter.php +6 -0
  117. package/server/src/Http/Filter/SensorFilter.php +125 -0
  118. package/server/src/Http/Filter/VehicleFilter.php +19 -0
  119. package/server/src/Http/Requests/CreateDriverRequest.php +14 -13
  120. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -0
  121. package/server/src/Http/Resources/v1/Driver.php +11 -2
  122. package/server/src/Http/Resources/v1/WorkOrder.php +1 -0
  123. package/server/src/Jobs/FinalizeApiOrderCreation.php +47 -0
  124. package/server/src/Jobs/FinalizeInternalOrderCreation.php +35 -0
  125. package/server/src/Jobs/NotifyBulkAssignedDriver.php +47 -0
  126. package/server/src/Jobs/SyncTelematicDevicesJob.php +181 -66
  127. package/server/src/Models/Device.php +15 -1
  128. package/server/src/Models/DeviceEvent.php +1 -1
  129. package/server/src/Models/Driver.php +17 -1
  130. package/server/src/Models/Sensor.php +15 -2
  131. package/server/src/Models/Telematic.php +1 -1
  132. package/server/src/Models/Vehicle.php +5 -1
  133. package/server/src/Models/WorkOrder.php +5 -2
  134. package/server/src/Observers/DriverObserver.php +3 -3
  135. package/server/src/Observers/FleetObserver.php +23 -0
  136. package/server/src/Observers/VehicleObserver.php +3 -3
  137. package/server/src/Providers/FleetOpsServiceProvider.php +12 -0
  138. package/server/src/Support/LiveCacheService.php +1 -1
  139. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +188 -28
  140. package/server/src/Support/Telematics/Providers/SafeeProvider.php +44 -3
  141. package/server/src/Support/Telematics/TelematicService.php +330 -30
  142. package/server/src/routes.php +10 -0
  143. package/server/tests/DeviceFilterTest.php +36 -0
  144. package/server/tests/HubRoutesTest.php +62 -0
  145. package/server/tests/OperationsMonitorEndpointTest.php +38 -0
  146. package/server/tests/OrderQueuedClosureRegressionTest.php +36 -0
  147. package/server/tests/TelematicsHardeningTest.php +455 -3
  148. package/server/tests/VehicleDriverAssignmentTest.php +27 -0
  149. package/translations/ar-ae.yml +1 -1
  150. package/translations/en-us.yaml +1 -0
  151. package/translations/vi-vn.yaml +1 -1
@@ -5,78 +5,136 @@ import { tracked } from '@glimmer/tracking';
5
5
 
6
6
  export default class ConnectivityTelematicsDetailsAttachmentsController extends Controller {
7
7
  @service deviceActions;
8
+ @service fetch;
8
9
  @service hostRouter;
9
10
  @service intl;
10
11
  @service modalsManager;
11
12
  @service notifications;
12
13
 
13
- @tracked queryParams = ['query', 'status', 'attachment_state', 'vehicle', 'sort'];
14
+ @tracked queryParams = ['query', 'status', 'vehicle', 'sort'];
14
15
  @tracked telematic;
15
16
  @tracked query;
16
17
  @tracked status;
17
- @tracked attachment_state;
18
18
  @tracked vehicle;
19
19
  @tracked sort = '-updated_at';
20
+ @tracked selectedDevice = null;
21
+ @tracked selectedVehicle = null;
22
+ @tracked isRefreshing = false;
20
23
 
21
24
  get syncedDevices() {
22
- return Array.from(this.model ?? []);
25
+ return Array.from(this.model?.devices ?? this.model ?? []);
23
26
  }
24
27
 
25
- get devices() {
26
- const query = String(this.query ?? '')
27
- .trim()
28
- .toLowerCase();
29
-
30
- return this.syncedDevices.filter((device) => {
31
- const matchesQuery =
32
- !query ||
33
- [device.displayName, device.name, device.device_id, device.serial_number, device.imei, device.attached_to_name, device.status, device.connection_status]
34
- .filter(Boolean)
35
- .some((value) => String(value).toLowerCase().includes(query));
28
+ get loadError() {
29
+ return this.model?.error;
30
+ }
36
31
 
37
- const matchesStatus = !this.status || device.status === this.status || device.connection_status === this.status;
38
- const matchesAttachmentState =
39
- !this.attachment_state || (this.attachment_state === 'attached' && device.attachable_uuid) || (this.attachment_state === 'unattached' && !device.attachable_uuid);
40
- const matchesVehicle = !this.vehicle || device.attachable_uuid === this.vehicle;
32
+ get totalSyncedDevices() {
33
+ return this.model?.meta?.total ?? this.syncedDevices.length;
34
+ }
41
35
 
42
- return matchesQuery && matchesStatus && matchesAttachmentState && matchesVehicle;
43
- });
36
+ get devices() {
37
+ return [...this.unattachedDevices, ...this.attachedDevices];
44
38
  }
45
39
 
46
40
  get unattachedDevices() {
47
- return this.devices.filter((device) => !device.attachable_uuid);
41
+ return this.syncedDevices.filter((device) => !device.attachable_uuid && this.deviceMatchesQuery(device) && this.deviceMatchesStatus(device));
48
42
  }
49
43
 
50
44
  get attachedDevices() {
51
- return this.devices.filter((device) => device.attachable_uuid);
45
+ return this.syncedDevices.filter((device) => device.attachable_uuid && this.deviceMatchesQuery(device) && this.deviceMatchesStatus(device) && this.deviceMatchesVehicle(device));
52
46
  }
53
47
 
54
48
  get onlineDevicesCount() {
55
- return this.devices.filter((device) => device.is_online || device.connection_status === 'online' || device.status === 'online' || device.status === 'active').length;
49
+ return this.syncedDevices.filter((device) => device.is_online || device.connection_status === 'online' || device.status === 'online' || device.status === 'active').length;
50
+ }
51
+
52
+ get attachedDevicesCount() {
53
+ return this.syncedDevices.filter((device) => device.attachable_uuid).length;
54
+ }
55
+
56
+ get unattachedDevicesCount() {
57
+ return this.syncedDevices.filter((device) => !device.attachable_uuid).length;
58
+ }
59
+
60
+ get mappedVehiclesCount() {
61
+ const vehicleIds = new Set(this.syncedDevices.filter((device) => device.attachable_uuid).map((device) => device.attachable_uuid));
62
+
63
+ return vehicleIds.size;
56
64
  }
57
65
 
58
66
  get vehicleGroups() {
59
67
  const groups = new Map();
60
68
 
61
69
  for (const device of this.attachedDevices) {
62
- const key = device.attachable_uuid;
70
+ this.addDeviceToVehicleGroup(groups, device);
71
+ }
72
+
73
+ return this.sortVehicleGroups(groups);
74
+ }
63
75
 
64
- if (!groups.has(key)) {
65
- groups.set(key, {
66
- id: key,
67
- name: device.attached_to_name ?? 'Unknown vehicle',
68
- devices: [],
69
- });
70
- }
76
+ addDeviceToVehicleGroup(groups, device) {
77
+ const key = device.attachable_uuid;
71
78
 
72
- groups.get(key).devices.push(device);
79
+ if (!groups.has(key)) {
80
+ const vehicle = device.attachable;
81
+
82
+ groups.set(key, {
83
+ id: key,
84
+ name: this.getDeviceVehicleName(device),
85
+ vehicle,
86
+ devices: [],
87
+ });
73
88
  }
74
89
 
90
+ groups.get(key).devices.push(device);
91
+ }
92
+
93
+ sortVehicleGroups(groups) {
75
94
  return Array.from(groups.values()).sort((a, b) => String(a.name).localeCompare(String(b.name)));
76
95
  }
77
96
 
97
+ get normalizedQuery() {
98
+ return String(this.query ?? '')
99
+ .trim()
100
+ .toLowerCase();
101
+ }
102
+
103
+ deviceMatchesQuery(device) {
104
+ const query = this.normalizedQuery;
105
+
106
+ if (!query) {
107
+ return true;
108
+ }
109
+
110
+ return [
111
+ device.displayName,
112
+ device.name,
113
+ device.device_id,
114
+ device.serial_number,
115
+ device.imei,
116
+ device.public_id,
117
+ device.attached_to_name,
118
+ device.attachable?.displayName,
119
+ device.attachable?.display_name,
120
+ device.attachable?.name,
121
+ device.status,
122
+ device.connection_status,
123
+ ]
124
+ .filter(Boolean)
125
+ .some((value) => String(value).toLowerCase().includes(query));
126
+ }
127
+
128
+ deviceMatchesStatus(device) {
129
+ return !this.status || device.status === this.status || device.connection_status === this.status;
130
+ }
131
+
132
+ deviceMatchesVehicle(device) {
133
+ return !this.vehicle || device.attachable_uuid === this.vehicle;
134
+ }
135
+
78
136
  get hasActiveFilters() {
79
- return Boolean(this.query || this.status || this.attachment_state || this.vehicle);
137
+ return Boolean(this.query || this.status || this.vehicle);
80
138
  }
81
139
 
82
140
  get hasSyncedDevices() {
@@ -96,6 +154,10 @@ export default class ConnectivityTelematicsDetailsAttachmentsController extends
96
154
  }
97
155
 
98
156
  get emptyStateVariant() {
157
+ if (this.loadError) {
158
+ return 'error';
159
+ }
160
+
99
161
  if (!this.hasSyncedDevices) {
100
162
  return 'not_synced';
101
163
  }
@@ -117,6 +179,19 @@ export default class ConnectivityTelematicsDetailsAttachmentsController extends
117
179
 
118
180
  get emptyStateContent() {
119
181
  switch (this.emptyStateVariant) {
182
+ case 'error':
183
+ return {
184
+ tone: 'danger',
185
+ icon: 'triangle-exclamation',
186
+ title: 'Unable to load device attachments',
187
+ message: 'Fleetbase could not load the full provider device list. Refresh the tab or review the connection logs.',
188
+ primaryText: 'Refresh',
189
+ primaryIcon: 'refresh',
190
+ primaryAction: this.refresh,
191
+ secondaryText: 'Go to Logs',
192
+ secondaryIcon: 'history',
193
+ secondaryAction: this.goToLogs,
194
+ };
120
195
  case 'not_synced':
121
196
  return {
122
197
  tone: 'warning',
@@ -158,36 +233,96 @@ export default class ConnectivityTelematicsDetailsAttachmentsController extends
158
233
 
159
234
  get metrics() {
160
235
  return [
161
- { label: 'Vehicles mapped', value: this.vehicleGroups.length, icon: 'truck', accentClass: 'fleetops-connectivity-kpi-accent-blue' },
162
- { label: 'Attached devices', value: this.attachedDevices.length, icon: 'link', accentClass: 'fleetops-connectivity-kpi-accent-green' },
163
- { label: 'Unattached devices', value: this.unattachedDevices.length, icon: 'link-slash', accentClass: 'fleetops-connectivity-kpi-accent-amber' },
236
+ { label: 'Synced devices', value: this.totalSyncedDevices, icon: 'microchip', accentClass: 'fleetops-connectivity-kpi-accent-blue' },
237
+ { label: 'Vehicles mapped', value: this.mappedVehiclesCount, icon: 'truck', accentClass: 'fleetops-connectivity-kpi-accent-blue' },
238
+ { label: 'Attached devices', value: this.attachedDevicesCount, icon: 'link', accentClass: 'fleetops-connectivity-kpi-accent-green' },
239
+ { label: 'Unattached devices', value: this.unattachedDevicesCount, icon: 'link-slash', accentClass: 'fleetops-connectivity-kpi-accent-amber' },
164
240
  { label: 'Online devices', value: this.onlineDevicesCount, icon: 'signal', accentClass: 'fleetops-connectivity-kpi-accent-green' },
165
241
  ];
166
242
  }
167
243
 
168
- @action updateQuery(event) {
169
- this.query = event.target.value;
244
+ get visibleDevicesCount() {
245
+ return this.devices.length;
246
+ }
247
+
248
+ get filteredUnattachedCount() {
249
+ return this.unattachedDevices.length;
250
+ }
251
+
252
+ get filteredAttachedCount() {
253
+ return this.attachedDevices.length;
170
254
  }
171
255
 
172
- @action updateAttachmentState(event) {
173
- this.attachment_state = event.target.value || null;
256
+ get selectedDeviceName() {
257
+ return this.selectedDevice?.displayName ?? this.selectedDevice?.name ?? this.selectedDevice?.device_id;
258
+ }
259
+
260
+ @action updateQuery(event) {
261
+ this.query = event.target.value;
174
262
  }
175
263
 
176
264
  @action updateStatus(event) {
177
265
  this.status = event.target.value || null;
178
266
  }
179
267
 
268
+ @action updateVehicle(valueOrEvent) {
269
+ const value = valueOrEvent?.target ? valueOrEvent.target.value : valueOrEvent;
270
+
271
+ this.vehicle = value || null;
272
+ }
273
+
274
+ @action updateSelectedVehicle(vehicle) {
275
+ this.selectedVehicle = vehicle;
276
+ this.vehicle = vehicle?.id ?? null;
277
+ }
278
+
180
279
  @action clearFilters() {
181
280
  this.query = null;
182
281
  this.status = null;
183
- this.attachment_state = null;
184
282
  this.vehicle = null;
283
+ this.selectedVehicle = null;
185
284
  }
186
285
 
187
286
  @action goToDevices() {
188
287
  return this.hostRouter.transitionTo('console.fleet-ops.connectivity.telematics.details.devices', this.telematic);
189
288
  }
190
289
 
290
+ @action goToLogs() {
291
+ return this.hostRouter.transitionTo('console.fleet-ops.connectivity.telematics.details.logs', this.telematic);
292
+ }
293
+
294
+ @action async refresh() {
295
+ this.isRefreshing = true;
296
+
297
+ try {
298
+ return await this.hostRouter.refresh();
299
+ } finally {
300
+ this.isRefreshing = false;
301
+ }
302
+ }
303
+
304
+ @action selectUnattachedDevice(device) {
305
+ if (this.selectedDevice === device) {
306
+ this.selectedDevice = null;
307
+
308
+ return;
309
+ }
310
+
311
+ this.selectedDevice = device;
312
+ }
313
+
314
+ @action clearSelectedDevice() {
315
+ this.selectedDevice = null;
316
+ }
317
+
318
+ @action attachSelectedDeviceToGroup(group) {
319
+ if (!this.selectedDevice) {
320
+ return;
321
+ }
322
+
323
+ return this.attachDeviceToVehicle(this.selectedDevice, group.vehicle ?? { id: group.id, name: group.name });
324
+ }
325
+
191
326
  @action openAttachDeviceModal(device) {
192
327
  this.modalsManager.show('modals/attach-telematic-device', {
193
328
  title: this.intl.t('device.prompts.attach-device-to-vehicle-title', { deviceName: device.displayName ?? device.name ?? device.device_id ?? this.intl.t('resource.device') }),
@@ -200,20 +335,12 @@ export default class ConnectivityTelematicsDetailsAttachmentsController extends
200
335
  return;
201
336
  }
202
337
 
203
- device.setProperties({
204
- attachable_uuid: selectedVehicle.id,
205
- attachable_type: 'fleet-ops:vehicle',
206
- });
207
-
208
338
  modal.startLoading();
209
339
 
210
340
  try {
211
- await device.save();
212
- this.notifications.success(this.intl.t('device.prompts.attach-to-vehicle-success'));
213
- await this.hostRouter.refresh();
341
+ await this.attachDeviceToVehicle(device, selectedVehicle);
214
342
  modal.done();
215
343
  } catch (error) {
216
- device.rollbackAttributes();
217
344
  this.notifications.serverError(error);
218
345
  modal.stopLoading();
219
346
  }
@@ -221,6 +348,14 @@ export default class ConnectivityTelematicsDetailsAttachmentsController extends
221
348
  });
222
349
  }
223
350
 
351
+ async attachDeviceToVehicle(device, selectedVehicle) {
352
+ const response = await this.fetch.post(`devices/${device.id}/attach`, { vehicle: selectedVehicle.id });
353
+
354
+ this.applyDeviceAttachment(device, selectedVehicle, response?.device);
355
+ this.selectedDevice = null;
356
+ this.notifications.success(this.intl.t('device.prompts.attach-to-vehicle-success'));
357
+ }
358
+
224
359
  @action detachDevice(device) {
225
360
  const deviceName = device.displayName ?? device.name ?? device.device_id ?? this.intl.t('resource.device');
226
361
 
@@ -229,19 +364,50 @@ export default class ConnectivityTelematicsDetailsAttachmentsController extends
229
364
  body: this.intl.t('device.prompts.detach-telematic-device-body', { deviceName }),
230
365
  confirm: async (modal) => {
231
366
  modal.startLoading();
232
- device.setProperties({ attachable_uuid: null, attachable_type: null });
233
367
 
234
368
  try {
235
- await device.save();
369
+ const response = await this.fetch.post(`devices/${device.id}/detach`);
370
+
371
+ this.applyDeviceDetachment(device, response?.device);
236
372
  this.notifications.success(this.intl.t('device.prompts.detach-from-vehicle-success'));
237
- await this.hostRouter.refresh();
238
373
  modal.done();
239
374
  } catch (error) {
240
- device.rollbackAttributes();
241
375
  this.notifications.serverError(error);
242
376
  modal.stopLoading();
243
377
  }
244
378
  },
245
379
  });
246
380
  }
381
+
382
+ getDeviceVehicleName(device) {
383
+ return device.attached_to_name ?? device.attachable?.display_name ?? device.attachable?.name ?? 'Unknown vehicle';
384
+ }
385
+
386
+ applyDeviceAttachment(device, selectedVehicle, serverDevice = {}) {
387
+ this.updateDevice(device, {
388
+ attachable_uuid: serverDevice.attachable_uuid ?? selectedVehicle.id,
389
+ attachable_type: serverDevice.attachable_type ?? 'fleet-ops:vehicle',
390
+ attached_to_name: serverDevice.attached_to_name ?? selectedVehicle.displayName ?? selectedVehicle.display_name ?? selectedVehicle.name,
391
+ attachable: serverDevice.attachable ?? selectedVehicle,
392
+ });
393
+ }
394
+
395
+ applyDeviceDetachment(device, serverDevice = {}) {
396
+ this.updateDevice(device, {
397
+ attachable_uuid: serverDevice.attachable_uuid ?? null,
398
+ attachable_type: serverDevice.attachable_type ?? null,
399
+ attached_to_name: serverDevice.attached_to_name ?? null,
400
+ attachable: serverDevice.attachable ?? null,
401
+ });
402
+ }
403
+
404
+ updateDevice(device, properties) {
405
+ if (typeof device.setProperties === 'function') {
406
+ device.setProperties(properties);
407
+
408
+ return;
409
+ }
410
+
411
+ Object.assign(device, properties);
412
+ }
247
413
  }
@@ -3,7 +3,14 @@ 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
- import fleetOpsOptions from '../../../../utils/fleet-ops-options';
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
+ ];
7
14
 
8
15
  export default class ConnectivityTelematicsDetailsDevicesController extends Controller {
9
16
  @service deviceActions;
@@ -13,7 +20,7 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
13
20
  @service modalsManager;
14
21
  @service notifications;
15
22
 
16
- @tracked queryParams = ['page', 'limit', 'sort', 'query', 'status', 'provider', 'attachment_state', 'device_id'];
23
+ @tracked queryParams = ['page', 'limit', 'sort', 'query', 'status', 'provider', 'attachment_state', 'vehicle', 'connection_status', 'device_id', 'last_online_at', 'updated_at'];
17
24
  @tracked telematic;
18
25
  @tracked page = 1;
19
26
  @tracked limit;
@@ -22,7 +29,11 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
22
29
  @tracked status;
23
30
  @tracked provider;
24
31
  @tracked attachment_state;
32
+ @tracked vehicle;
33
+ @tracked connection_status;
25
34
  @tracked device_id;
35
+ @tracked last_online_at;
36
+ @tracked updated_at;
26
37
 
27
38
  get devices() {
28
39
  return Array.from(this.model ?? []);
@@ -45,7 +56,9 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
45
56
  }
46
57
 
47
58
  get hasActiveFilters() {
48
- return Boolean(this.query || this.status || this.provider || this.attachment_state || this.device_id);
59
+ 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
61
+ );
49
62
  }
50
63
 
51
64
  get hasDevices() {
@@ -145,35 +158,39 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
145
158
  ];
146
159
  }
147
160
 
148
- @tracked actionButtons = [
149
- {
150
- icon: 'refresh',
151
- size: 'xs',
152
- onClick: this.refresh,
153
- helpText: this.intl.t('common.refresh'),
154
- wrapperClass: 'fleetops-telematics-action-button',
155
- },
156
- {
157
- icon: 'ellipsis-h',
158
- prefix: 'fas',
159
- text: 'Actions',
160
- type: 'primary',
161
- size: 'xs',
162
- triggerClass: 'fleetops-telematics-action-button',
163
- items: [
164
- {
165
- icon: 'satellite-dish',
166
- text: 'Sync Devices',
167
- onClick: this.startDeviceSync,
168
- },
169
- {
170
- icon: 'link',
171
- text: 'Attach Unassigned',
172
- onClick: this.openAttachmentsForUnassigned,
173
- },
174
- ],
175
- },
176
- ];
161
+ get actionButtons() {
162
+ return [
163
+ {
164
+ icon: 'refresh',
165
+ size: 'sm',
166
+ onClick: this.refresh,
167
+ helpText: this.intl.t('common.refresh'),
168
+ wrapperClass: 'fleetops-telematics-action-button',
169
+ isLoading: this.refreshTask.isRunning,
170
+ disabled: this.refreshTask.isRunning,
171
+ },
172
+ {
173
+ icon: 'ellipsis-h',
174
+ prefix: 'fas',
175
+ helpText: 'Actions',
176
+ type: 'primary',
177
+ size: 'sm',
178
+ triggerClass: 'fleetops-telematics-action-button',
179
+ items: [
180
+ {
181
+ icon: 'satellite-dish',
182
+ text: 'Sync Devices',
183
+ onClick: this.startDeviceSync,
184
+ },
185
+ {
186
+ icon: 'link',
187
+ text: 'Attach Unassigned',
188
+ onClick: this.openAttachmentsForUnassigned,
189
+ },
190
+ ],
191
+ },
192
+ ];
193
+ }
177
194
 
178
195
  @tracked bulkActions = [];
179
196
 
@@ -207,12 +224,11 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
207
224
  resizable: true,
208
225
  sortable: true,
209
226
  filterable: true,
210
- filterParam: 'attachment_state',
211
- filterComponent: 'filter/multi-option',
212
- filterOptions: [
213
- { label: 'Attached', value: 'attached' },
214
- { label: 'Unattached', value: 'unattached' },
215
- ],
227
+ filterParam: 'vehicle',
228
+ filterComponent: 'filter/model',
229
+ filterComponentPlaceholder: 'Select vehicle',
230
+ model: 'vehicle',
231
+ modelNamePath: 'displayName',
216
232
  },
217
233
  {
218
234
  label: 'Connection',
@@ -221,9 +237,11 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
221
237
  resizable: true,
222
238
  sortable: true,
223
239
  filterable: true,
224
- filterParam: 'status',
240
+ filterParam: 'connection_status',
225
241
  filterComponent: 'filter/multi-option',
226
- filterOptions: fleetOpsOptions('deviceStatuses'),
242
+ filterOptions: connectionStatusOptions,
243
+ filterOptionLabel: 'label',
244
+ filterOptionValue: 'value',
227
245
  },
228
246
  {
229
247
  label: 'Last Seen',
@@ -231,6 +249,25 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
231
249
  sortParam: 'last_online_at',
232
250
  resizable: true,
233
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',
234
271
  },
235
272
  {
236
273
  label: this.intl.t('column.updated-at'),
@@ -284,7 +321,11 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
284
321
  }
285
322
 
286
323
  @action refresh() {
287
- return this.hostRouter.refresh();
324
+ if (this.refreshTask.isRunning) {
325
+ return;
326
+ }
327
+
328
+ return this.refreshTask.perform();
288
329
  }
289
330
 
290
331
  @action clearFilters() {
@@ -292,7 +333,11 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
292
333
  this.status = null;
293
334
  this.provider = null;
294
335
  this.attachment_state = null;
336
+ this.vehicle = null;
337
+ this.connection_status = null;
295
338
  this.device_id = null;
339
+ this.last_online_at = null;
340
+ this.updated_at = null;
296
341
  this.page = 1;
297
342
  }
298
343
 
@@ -356,6 +401,10 @@ export default class ConnectivityTelematicsDetailsDevicesController extends Cont
356
401
  return this.syncDevices.perform();
357
402
  }
358
403
 
404
+ @task *refreshTask() {
405
+ yield this.hostRouter.refresh();
406
+ }
407
+
359
408
  @task *syncDevices() {
360
409
  try {
361
410
  yield this.fetch.post(`telematics/${this.telematic.id}/discover`);