@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,7 +1,7 @@
1
1
  import Controller from '@ember/controller';
2
2
  import { inject as service } from '@ember/service';
3
3
  import { tracked } from '@glimmer/tracking';
4
- import { action } from '@ember/object';
4
+ import { action, get } from '@ember/object';
5
5
 
6
6
  export default class ManagementDriversIndexController extends Controller {
7
7
  @service driverActions;
@@ -52,6 +52,22 @@ export default class ManagementDriversIndexController extends Controller {
52
52
  return Boolean(driver?.vehicle_uuid || driver?.vehicle?.id || driver?.vehicle?.uuid || driver?.vehicle_name);
53
53
  }
54
54
 
55
+ async resolveAssignedVehicle(vehicle) {
56
+ if (!vehicle) {
57
+ return null;
58
+ }
59
+
60
+ if (typeof vehicle.then === 'function') {
61
+ return await vehicle;
62
+ }
63
+
64
+ if (typeof vehicle.loadResource === 'function') {
65
+ return (await vehicle.loadResource()) ?? vehicle;
66
+ }
67
+
68
+ return vehicle;
69
+ }
70
+
55
71
  /** action buttons */
56
72
  get actionButtons() {
57
73
  return [
@@ -102,7 +118,8 @@ export default class ManagementDriversIndexController extends Controller {
102
118
  sticky: true,
103
119
  label: this.intl.t('column.name'),
104
120
  valuePath: 'name',
105
- cellComponent: 'table/cell/driver-name',
121
+ cellComponent: 'cell/driver-identity',
122
+ compact: true,
106
123
  permission: 'fleet-ops view driver',
107
124
  action: this.driverActions.transition.view,
108
125
  resizable: true,
@@ -120,6 +137,69 @@ export default class ManagementDriversIndexController extends Controller {
120
137
  hidden: false,
121
138
  filterComponent: 'filter/string',
122
139
  },
140
+ {
141
+ label: this.intl.t('column.phone'),
142
+ valuePath: 'phone',
143
+ cellComponent: 'table/cell/base',
144
+ resizable: true,
145
+ sortable: true,
146
+ filterable: true,
147
+ filterParam: 'phone',
148
+ filterComponent: 'filter/string',
149
+ },
150
+ {
151
+ label: this.intl.t('column.license'),
152
+ valuePath: 'drivers_license_number',
153
+ cellComponent: 'table/cell/base',
154
+ resizable: true,
155
+ sortable: true,
156
+ filterable: true,
157
+ filterComponent: 'filter/string',
158
+ },
159
+ {
160
+ label: this.intl.t('column.vehicle'),
161
+ cellComponent: 'cell/vehicle-identity',
162
+ compact: true,
163
+ permission: 'fleet-ops view vehicle',
164
+ action: async (vehicle) => {
165
+ try {
166
+ const resolvedVehicle = await this.resolveAssignedVehicle(vehicle);
167
+ if (resolvedVehicle) this.vehicleActions.panel.view(resolvedVehicle);
168
+ } catch (err) {
169
+ this.notifications.serverError(err);
170
+ }
171
+ },
172
+ valuePath: 'vehicle.display_name',
173
+ emptyText: '-',
174
+ showStatusBadge: true,
175
+ resourcePath: (driver) => {
176
+ const vehicle = get(driver, 'vehicle_assigned') ?? get(driver, 'vehicle');
177
+
178
+ if (vehicle) {
179
+ return vehicle;
180
+ }
181
+
182
+ const vehicleName = get(driver, 'vehicle_name');
183
+
184
+ if (vehicleName) {
185
+ return {
186
+ display_name: vehicleName,
187
+ name: vehicleName,
188
+ status: 'assigned',
189
+ loadResource: () => driver.loadVehicle?.(),
190
+ };
191
+ }
192
+
193
+ return null;
194
+ },
195
+ modelNamePath: 'display_name',
196
+ resizable: true,
197
+ filterable: true,
198
+ filterComponent: 'filter/model',
199
+ filterComponentPlaceholder: 'Select vehicle to filter by',
200
+ filterParam: 'vehicle',
201
+ model: 'vehicle',
202
+ },
123
203
  {
124
204
  label: this.intl.t('column.internal-id'),
125
205
  valuePath: 'internal_id',
@@ -150,27 +230,6 @@ export default class ManagementDriversIndexController extends Controller {
150
230
  filterParam: 'vendor',
151
231
  model: 'vendor',
152
232
  },
153
- {
154
- label: this.intl.t('column.vehicle'),
155
- cellComponent: 'table/cell/anchor',
156
- permission: 'fleet-ops view vehicle',
157
- onClick: async (driver) => {
158
- try {
159
- const vehicle = await driver.loadVehicle();
160
- if (vehicle) this.vehicleActions.panel.view(vehicle);
161
- } catch (err) {
162
- this.notifications.serverError(err);
163
- }
164
- },
165
- valuePath: 'vehicle.display_name',
166
- modelNamePath: 'display_name',
167
- resizable: true,
168
- filterable: true,
169
- filterComponent: 'filter/model',
170
- filterComponentPlaceholder: 'Select vehicle to filter by',
171
- filterParam: 'vehicle',
172
- model: 'vehicle',
173
- },
174
233
  {
175
234
  label: this.intl.t('column.fleet'),
176
235
  cellComponent: 'table/cell/link-list',
@@ -185,25 +244,6 @@ export default class ManagementDriversIndexController extends Controller {
185
244
  filterParam: 'fleet',
186
245
  model: 'fleet',
187
246
  },
188
- {
189
- label: this.intl.t('column.license'),
190
- valuePath: 'drivers_license_number',
191
- cellComponent: 'table/cell/base',
192
- resizable: true,
193
- sortable: true,
194
- filterable: true,
195
- filterComponent: 'filter/string',
196
- },
197
- {
198
- label: this.intl.t('column.phone'),
199
- valuePath: 'phone',
200
- cellComponent: 'table/cell/base',
201
- resizable: true,
202
- sortable: true,
203
- filterable: true,
204
- filterParam: 'phone',
205
- filterComponent: 'filter/string',
206
- },
207
247
  {
208
248
  label: this.intl.t('column.country'),
209
249
  valuePath: 'country',
@@ -1,4 +1,5 @@
1
1
  import Controller from '@ember/controller';
2
+ import { get } from '@ember/object';
2
3
  import { inject as service } from '@ember/service';
3
4
  import { tracked } from '@glimmer/tracking';
4
5
 
@@ -113,9 +114,37 @@ export default class ManagementFuelReportsIndexController extends Controller {
113
114
  {
114
115
  label: this.intl.t('column.driver'),
115
116
  valuePath: 'driver_name',
116
- cellComponent: 'table/cell/anchor',
117
+ cellComponent: 'cell/driver-identity',
117
118
  permission: 'fleet-ops view driver',
118
- onClick: this.fuelReportActions.viewDriver,
119
+ action: async (driver) => {
120
+ const resolvedDriver = driver?.loadResource ? await driver.loadResource() : driver;
121
+
122
+ if (resolvedDriver) {
123
+ this.fuelReportActions.driverActions.panel.view(resolvedDriver);
124
+ }
125
+ },
126
+ emptyText: '-',
127
+ showStatusBadge: true,
128
+ resourcePath: (fuelReport) => {
129
+ const driver = get(fuelReport, 'driver');
130
+
131
+ if (driver) {
132
+ return driver;
133
+ }
134
+
135
+ const driverName = get(fuelReport, 'driver_name');
136
+
137
+ if (driverName) {
138
+ return {
139
+ id: get(fuelReport, 'driver_uuid'),
140
+ name: driverName,
141
+ display_name: driverName,
142
+ loadResource: () => fuelReport.loadDriver?.(),
143
+ };
144
+ }
145
+
146
+ return null;
147
+ },
119
148
  resizable: true,
120
149
  sortable: true,
121
150
  filterable: true,
@@ -127,9 +156,39 @@ export default class ManagementFuelReportsIndexController extends Controller {
127
156
  {
128
157
  label: this.intl.t('column.vehicle'),
129
158
  valuePath: 'vehicle_name',
130
- cellComponent: 'table/cell/anchor',
159
+ cellComponent: 'cell/vehicle-identity',
131
160
  permission: 'fleet-ops view vehicle',
132
- onClick: this.fuelReportActions.viewVehicle,
161
+ action: async (vehicle) => {
162
+ const resolvedVehicle = vehicle?.loadResource ? await vehicle.loadResource() : vehicle;
163
+
164
+ if (resolvedVehicle) {
165
+ this.fuelReportActions.vehicleActions.panel.view(resolvedVehicle);
166
+ }
167
+ },
168
+ emptyText: '-',
169
+ showStatusBadge: true,
170
+ resourcePath: (fuelReport) => {
171
+ const vehicle = get(fuelReport, 'vehicle');
172
+
173
+ if (vehicle) {
174
+ return vehicle;
175
+ }
176
+
177
+ const vehicleName = get(fuelReport, 'vehicle_name');
178
+
179
+ if (vehicleName) {
180
+ return {
181
+ id: get(fuelReport, 'vehicle_uuid'),
182
+ displayName: vehicleName,
183
+ display_name: vehicleName,
184
+ name: vehicleName,
185
+ vehicle_number: get(fuelReport, 'vehicle_uuid'),
186
+ loadResource: () => fuelReport.loadVehicle?.(),
187
+ };
188
+ }
189
+
190
+ return null;
191
+ },
133
192
  resizable: true,
134
193
  sortable: true,
135
194
  filterable: true,
@@ -1,4 +1,5 @@
1
1
  import Controller from '@ember/controller';
2
+ import { 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';
@@ -162,9 +163,37 @@ export default class ManagementIssuesIndexController extends Controller {
162
163
  {
163
164
  label: this.intl.t('column.driver'),
164
165
  valuePath: 'driver_name',
165
- cellComponent: 'table/cell/anchor',
166
+ cellComponent: 'cell/driver-identity',
166
167
  permission: 'fleet-ops view driver',
167
- onClick: this.issueActions.viewDriver,
168
+ action: async (driver) => {
169
+ const resolvedDriver = driver?.loadResource ? await driver.loadResource() : driver;
170
+
171
+ if (resolvedDriver) {
172
+ this.issueActions.driverActions.panel.view(resolvedDriver);
173
+ }
174
+ },
175
+ emptyText: '-',
176
+ showStatusBadge: true,
177
+ resourcePath: (issue) => {
178
+ const driver = get(issue, 'driver');
179
+
180
+ if (driver) {
181
+ return driver;
182
+ }
183
+
184
+ const driverName = get(issue, 'driver_name');
185
+
186
+ if (driverName) {
187
+ return {
188
+ id: get(issue, 'driver_uuid'),
189
+ name: driverName,
190
+ display_name: driverName,
191
+ loadResource: () => issue.loadDriver?.(),
192
+ };
193
+ }
194
+
195
+ return null;
196
+ },
168
197
  resizable: true,
169
198
  sortable: true,
170
199
  filterable: true,
@@ -176,9 +205,39 @@ export default class ManagementIssuesIndexController extends Controller {
176
205
  {
177
206
  label: this.intl.t('column.vehicle'),
178
207
  valuePath: 'vehicle_name',
179
- cellComponent: 'table/cell/anchor',
208
+ cellComponent: 'cell/vehicle-identity',
180
209
  permission: 'fleet-ops view vehicle',
181
- onClick: this.issueActions.viewVehicle,
210
+ action: async (vehicle) => {
211
+ const resolvedVehicle = vehicle?.loadResource ? await vehicle.loadResource() : vehicle;
212
+
213
+ if (resolvedVehicle) {
214
+ this.issueActions.vehicleActions.panel.view(resolvedVehicle);
215
+ }
216
+ },
217
+ emptyText: '-',
218
+ showStatusBadge: true,
219
+ resourcePath: (issue) => {
220
+ const vehicle = get(issue, 'vehicle');
221
+
222
+ if (vehicle) {
223
+ return vehicle;
224
+ }
225
+
226
+ const vehicleName = get(issue, 'vehicle_name');
227
+
228
+ if (vehicleName) {
229
+ return {
230
+ id: get(issue, 'vehicle_uuid'),
231
+ displayName: vehicleName,
232
+ display_name: vehicleName,
233
+ name: vehicleName,
234
+ vehicle_number: get(issue, 'vehicle_uuid'),
235
+ loadResource: () => issue.loadVehicle?.(),
236
+ };
237
+ }
238
+
239
+ return null;
240
+ },
182
241
  resizable: true,
183
242
  sortable: true,
184
243
  filterable: true,
@@ -1,5 +1,5 @@
1
1
  import Controller from '@ember/controller';
2
- import { action } from '@ember/object';
2
+ import { action, get } from '@ember/object';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { tracked } from '@glimmer/tracking';
5
5
 
@@ -126,7 +126,8 @@ export default class ManagementVehiclesIndexController extends Controller {
126
126
  label: this.intl.t('column.name'),
127
127
  valuePath: 'displayName',
128
128
  photoPath: 'avatar_url',
129
- cellComponent: 'table/cell/vehicle-name',
129
+ cellComponent: 'cell/vehicle-identity',
130
+ compact: true,
130
131
  permission: 'fleet-ops view vehicle',
131
132
  action: this.vehicleActions.transition.view,
132
133
  resizable: true,
@@ -135,6 +136,7 @@ export default class ManagementVehiclesIndexController extends Controller {
135
136
  filterComponent: 'filter/string',
136
137
  filterParam: 'name',
137
138
  showOnlineIndicator: true,
139
+ showStatus: false,
138
140
  },
139
141
  {
140
142
  label: this.intl.t('column.plate-number'),
@@ -160,14 +162,39 @@ export default class ManagementVehiclesIndexController extends Controller {
160
162
  },
161
163
  {
162
164
  label: this.intl.t('column.driver-assigned'),
163
- cellComponent: 'table/cell/anchor',
165
+ cellComponent: 'cell/driver-identity',
166
+ compact: true,
167
+ assignedVehicleLabel: (_driver, vehicle) => get(vehicle, 'displayName') ?? get(vehicle, 'display_name') ?? get(vehicle, 'name'),
164
168
  permission: 'fleet-ops view driver',
165
- action: async (vehicle) => {
166
- const driver = await vehicle.loadDriver();
169
+ action: async (driver) => {
170
+ const resolvedDriver = get(driver, 'id') ? driver : await driver?.loadResource?.();
167
171
 
168
- return this.driverActions.panel.view(driver);
172
+ if (resolvedDriver) {
173
+ return this.driverActions.panel.view(resolvedDriver);
174
+ }
169
175
  },
170
176
  valuePath: 'driver_name',
177
+ emptyText: '-',
178
+ resourcePath: (vehicle) => {
179
+ const driver = get(vehicle, 'driver');
180
+
181
+ if (driver) {
182
+ return driver;
183
+ }
184
+
185
+ const driverName = get(vehicle, 'driver_name');
186
+
187
+ if (driverName) {
188
+ return {
189
+ name: driverName,
190
+ display_name: driverName,
191
+ status: 'assigned',
192
+ loadResource: () => vehicle.loadDriver?.(),
193
+ };
194
+ }
195
+
196
+ return null;
197
+ },
171
198
  resizable: true,
172
199
  filterable: true,
173
200
  filterComponent: 'filter/model',
@@ -1,7 +1,29 @@
1
1
  import Route from '@ember/routing/route';
2
+ import { inject as service } from '@ember/service';
2
3
 
3
4
  export default class ConnectivityDevicesIndexDetailsVehicleRoute extends Route {
4
- model() {
5
- return this.modelFor('connectivity.devices.index.details');
5
+ @service store;
6
+
7
+ async model() {
8
+ const device = this.modelFor('connectivity.devices.index.details');
9
+ const vehicle = await Promise.resolve(device?.attachable);
10
+
11
+ if (!vehicle?.id) {
12
+ return { device, positions: [] };
13
+ }
14
+
15
+ let positions = [];
16
+
17
+ try {
18
+ positions = await this.store.query('position', {
19
+ subject_uuid: vehicle.id,
20
+ sort: '-created_at',
21
+ limit: 5,
22
+ });
23
+ } catch (_) {
24
+ positions = [];
25
+ }
26
+
27
+ return { device, positions };
6
28
  }
7
29
  }
@@ -14,6 +14,13 @@ export default class ConnectivityDevicesIndexRoute extends Route {
14
14
  status: { refreshModel: true },
15
15
  attachment_state: { refreshModel: true },
16
16
  telematic: { refreshModel: true },
17
+ provider: { refreshModel: true },
18
+ vehicle: { refreshModel: true },
19
+ connection_status: { refreshModel: true },
20
+ device_id: { refreshModel: true },
21
+ type: { refreshModel: true },
22
+ serial_number: { refreshModel: true },
23
+ last_online_at: { refreshModel: true },
17
24
  created_at: { refreshModel: true },
18
25
  updated_at: { refreshModel: true },
19
26
  };
@@ -0,0 +1,3 @@
1
+ import Route from '@ember/routing/route';
2
+
3
+ export default class ConnectivityEventsDetailsRoute extends Route {}
@@ -9,8 +9,12 @@ export default class ConnectivityEventsIndexRoute extends Route {
9
9
  limit: { refreshModel: true },
10
10
  sort: { refreshModel: true },
11
11
  query: { refreshModel: true },
12
- name: { refreshModel: true },
13
- public_id: { refreshModel: true },
12
+ telematic: { refreshModel: true },
13
+ device: { refreshModel: true },
14
+ event_type: { refreshModel: true },
15
+ severity: { refreshModel: true },
16
+ processed: { refreshModel: true },
17
+ occurred_at: { refreshModel: true },
14
18
  created_at: { refreshModel: true },
15
19
  updated_at: { refreshModel: true },
16
20
  };
@@ -9,8 +9,13 @@ export default class ConnectivitySensorsIndexRoute extends Route {
9
9
  limit: { refreshModel: true },
10
10
  sort: { refreshModel: true },
11
11
  query: { refreshModel: true },
12
- name: { refreshModel: true },
13
- public_id: { refreshModel: true },
12
+ telematic: { refreshModel: true },
13
+ device: { refreshModel: true },
14
+ type: { refreshModel: true },
15
+ status: { refreshModel: true },
16
+ serial_number: { refreshModel: true },
17
+ imei: { refreshModel: true },
18
+ last_reading_at: { refreshModel: true },
14
19
  created_at: { refreshModel: true },
15
20
  updated_at: { refreshModel: true },
16
21
  };
@@ -15,6 +15,8 @@ export default class ConnectivityTelematicsDetailsDevicesRoute extends Route {
15
15
  vehicle: { refreshModel: true },
16
16
  connection_status: { refreshModel: true },
17
17
  device_id: { refreshModel: true },
18
+ type: { refreshModel: true },
19
+ serial_number: { refreshModel: true },
18
20
  last_online_at: { refreshModel: true },
19
21
  updated_at: { refreshModel: true },
20
22
  };
@@ -1,8 +1,10 @@
1
1
  import ResourceActionService, { inject as service } from '@fleetbase/ember-core/services/resource-action';
2
2
  import { action } from '@ember/object';
3
+ import { isArray } from '@ember/array';
3
4
 
4
5
  export default class DeviceActionsService extends ResourceActionService {
5
6
  @service fetch;
7
+ @service('universe/menu-service') menuService;
6
8
 
7
9
  constructor() {
8
10
  super(...arguments);
@@ -37,13 +39,41 @@ export default class DeviceActionsService extends ResourceActionService {
37
39
  });
38
40
  },
39
41
  view: (device) => {
42
+ if (!device?.id) {
43
+ return this.notifications?.warning?.(this.intl.t('common.invalid-resource'));
44
+ }
45
+
46
+ const registeredTabs = this.menuService?.getMenuItems?.('fleet-ops:component:device:details');
47
+
40
48
  return this.resourceContextPanel.open({
41
49
  device,
50
+ header: 'device/panel-header',
42
51
  tabs: [
43
52
  {
53
+ key: 'overview',
54
+ id: 'overview',
44
55
  label: this.intl.t('common.overview'),
45
56
  component: 'device/details',
46
57
  },
58
+ {
59
+ key: 'vehicle',
60
+ id: 'vehicle',
61
+ label: this.intl.t('resource.vehicle'),
62
+ component: 'device/panel-tabs/vehicle',
63
+ },
64
+ {
65
+ key: 'sensors',
66
+ id: 'sensors',
67
+ label: this.intl.t('resource.sensors'),
68
+ component: 'device/panel-tabs/sensors',
69
+ },
70
+ {
71
+ key: 'events',
72
+ id: 'events',
73
+ label: this.intl.t('resource.device-events'),
74
+ component: 'device/panel-tabs/events',
75
+ },
76
+ ...(isArray(registeredTabs) ? registeredTabs.filter((tab) => tab.component || tab.render) : []),
47
77
  ],
48
78
  });
49
79
  },
@@ -11,7 +11,7 @@ export default class DeviceEventActionsService extends ResourceActionService {
11
11
  }
12
12
 
13
13
  transition = {
14
- view: (deviceEvent) => this.transitionTo('connectivity.events.index.details', deviceEvent),
14
+ view: (deviceEvent) => this.transitionTo('connectivity.events.details', deviceEvent),
15
15
  };
16
16
 
17
17
  panel = {
@@ -9,6 +9,7 @@ export default class EquipmentActionsService extends ResourceActionService {
9
9
  super(...arguments);
10
10
  this.initialize('equipment', {
11
11
  defaultAttributes: {
12
+ status: 'available',
12
13
  currency: this.defaultCurrency,
13
14
  },
14
15
  });
@@ -36,8 +36,11 @@ export default class SensorActionsService extends ResourceActionService {
36
36
  view: (sensor) => {
37
37
  return this.resourceContextPanel.open({
38
38
  sensor,
39
+ header: 'sensor/panel-header',
39
40
  tabs: [
40
41
  {
42
+ key: 'overview',
43
+ id: 'overview',
41
44
  label: this.intl.t('common.overview'),
42
45
  component: 'sensor/details',
43
46
  },
@@ -96,8 +96,15 @@ export default class ServiceRateActionsService extends ResourceActionService {
96
96
  };
97
97
 
98
98
  @task *queryServiceRatesForOrder(order, params = {}) {
99
+ const payloadCoordinates = order.payload?.payloadCoordinates ?? [];
100
+ const routeCoordinates = payloadCoordinates.map((coordinate) => this.#serializeRouteCoordinate(coordinate)).filter(Boolean);
101
+
102
+ if (routeCoordinates.length < 2) {
103
+ return [];
104
+ }
105
+
99
106
  const queryParams = {
100
- coordinates: order.payload.payloadCoordinates.join(';'),
107
+ coordinates: routeCoordinates.join(';'),
101
108
  facilitator: order.facilitator?.get('isIntegratedVendor') ? order.facilitator.get('public_id') : null,
102
109
  service_type: order.order_config?.get('key'),
103
110
  ...params,
@@ -117,6 +124,37 @@ export default class ServiceRateActionsService extends ResourceActionService {
117
124
  }
118
125
  }
119
126
 
127
+ #serializeRouteCoordinate(coordinate) {
128
+ if (Array.isArray(coordinate)) {
129
+ const [longitude, latitude] = coordinate;
130
+
131
+ if (!this.#isValidCoordinate(latitude) || !this.#isValidCoordinate(longitude)) {
132
+ return null;
133
+ }
134
+
135
+ return `${latitude},${longitude}`;
136
+ }
137
+
138
+ if (typeof coordinate === 'string') {
139
+ const parts = coordinate
140
+ .split(',')
141
+ .map((part) => part.trim())
142
+ .filter(Boolean);
143
+
144
+ if (parts.length !== 2 || !parts.every((part) => this.#isValidCoordinate(part))) {
145
+ return null;
146
+ }
147
+
148
+ return parts.join(',');
149
+ }
150
+
151
+ return null;
152
+ }
153
+
154
+ #isValidCoordinate(value) {
155
+ return value !== null && value !== undefined && value !== '' && Number.isFinite(Number(value));
156
+ }
157
+
120
158
  @task *getServiceQuotes(serviceRate, order) {
121
159
  if (order.payload.payloadCoordinates.length < 2) return;
122
160