@fleetbase/fleetops-engine 0.6.53 → 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 (212) 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/details.hbs +106 -135
  19. package/addon/components/device/details.js +174 -1
  20. package/addon/components/device/form.hbs +2 -2
  21. package/addon/components/device/form.js +7 -0
  22. package/addon/components/device/panel-header.hbs +46 -21
  23. package/addon/components/device/panel-header.js +49 -1
  24. package/addon/components/device/panel-tabs/events.hbs +30 -0
  25. package/addon/components/device/panel-tabs/events.js +128 -0
  26. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  27. package/addon/components/device/panel-tabs/sensors.js +93 -0
  28. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  29. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  30. package/addon/components/equipment/form.js +8 -1
  31. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +76 -11
  32. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  33. package/addon/components/layout/fleet-ops-sidebar.js +101 -17
  34. package/addon/components/map/drawer/device-event-listing.js +2 -0
  35. package/addon/components/map/drawer/driver-listing.js +2 -0
  36. package/addon/components/map/drawer/place-listing.js +2 -0
  37. package/addon/components/map/drawer/position-listing.hbs +11 -6
  38. package/addon/components/map/drawer/position-listing.js +17 -4
  39. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  40. package/addon/components/order/form/details.hbs +1 -43
  41. package/addon/components/order/form/details.js +18 -53
  42. package/addon/components/order/form/orchestrator-constraints.hbs +40 -0
  43. package/addon/components/order/form/orchestrator-constraints.js +61 -0
  44. package/addon/components/order/form/payload.js +48 -2
  45. package/addon/components/order/form/route.js +15 -0
  46. package/addon/components/order/form/service-rate.hbs +62 -11
  47. package/addon/components/order/form/service-rate.js +124 -2
  48. package/addon/components/order/form.hbs +2 -0
  49. package/addon/components/sensor/panel-header.hbs +47 -15
  50. package/addon/components/sensor/panel-header.js +59 -1
  51. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  52. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  53. package/addon/components/table/cell/dropdown.hbs +28 -0
  54. package/addon/components/table/cell/dropdown.js +91 -0
  55. package/addon/components/telematic/details.hbs +7 -1
  56. package/addon/components/telematic/form.js +16 -8
  57. package/addon/components/vendor/panel-header.hbs +82 -0
  58. package/addon/components/vendor/panel-header.js +48 -0
  59. package/addon/components/work-order/details.hbs +5 -1
  60. package/addon/components/work-order/form.hbs +26 -4
  61. package/addon/components/work-order/form.js +5 -1
  62. package/addon/controllers/connectivity/devices/index/details/events.js +244 -1
  63. package/addon/controllers/connectivity/devices/index/details/sensors.js +199 -0
  64. package/addon/controllers/connectivity/devices/index/details/vehicle.js +122 -0
  65. package/addon/controllers/connectivity/devices/index/details.js +12 -0
  66. package/addon/controllers/connectivity/devices/index.js +144 -129
  67. package/addon/controllers/connectivity/events/details.js +3 -0
  68. package/addon/controllers/connectivity/events/index.js +187 -11
  69. package/addon/controllers/connectivity/sensors/index.js +156 -14
  70. package/addon/controllers/connectivity/telematics/details/attachments.js +220 -54
  71. package/addon/controllers/connectivity/telematics/details/devices.js +170 -129
  72. package/addon/controllers/connectivity/telematics/details/events.js +39 -11
  73. package/addon/controllers/connectivity/telematics/details/sensors.js +39 -19
  74. package/addon/controllers/maintenance/equipment/index.js +1 -2
  75. package/addon/controllers/maintenance/parts/index.js +1 -2
  76. package/addon/controllers/maintenance/work-orders/index.js +18 -1
  77. package/addon/controllers/management/drivers/index.js +82 -42
  78. package/addon/controllers/management/fuel-reports/index.js +63 -4
  79. package/addon/controllers/management/issues/index.js +63 -4
  80. package/addon/controllers/management/vehicles/index.js +33 -6
  81. package/addon/routes/connectivity/devices/index/details/events.js +37 -1
  82. package/addon/routes/connectivity/devices/index/details/sensors.js +35 -0
  83. package/addon/routes/connectivity/devices/index/details/vehicle.js +29 -0
  84. package/addon/routes/connectivity/devices/index.js +7 -0
  85. package/addon/routes/connectivity/events/details.js +3 -0
  86. package/addon/routes/connectivity/events/index.js +6 -2
  87. package/addon/routes/connectivity/sensors/index.js +7 -2
  88. package/addon/routes/connectivity/telematics/details/attachments.js +72 -9
  89. package/addon/routes/connectivity/telematics/details/devices.js +6 -0
  90. package/addon/routes/connectivity/telematics/details/sensors.js +1 -0
  91. package/addon/routes/connectivity/telematics/details.js +1 -1
  92. package/addon/routes/connectivity/telematics/edit.js +1 -1
  93. package/addon/routes/maintenance/work-orders/index.js +1 -0
  94. package/addon/routes.js +2 -0
  95. package/addon/services/device-actions.js +30 -0
  96. package/addon/services/device-event-actions.js +1 -1
  97. package/addon/services/equipment-actions.js +1 -0
  98. package/addon/services/order-creation.js +54 -1
  99. package/addon/services/order-validation.js +3 -2
  100. package/addon/services/sensor-actions.js +3 -0
  101. package/addon/services/service-rate-actions.js +40 -1
  102. package/addon/services/vehicle-actions.js +24 -12
  103. package/addon/services/vendor-actions.js +3 -0
  104. package/addon/styles/fleetops-engine.css +28 -4
  105. package/addon/templates/connectivity/devices/index/details/events.hbs +85 -1
  106. package/addon/templates/connectivity/devices/index/details/sensors.hbs +73 -0
  107. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +147 -0
  108. package/addon/templates/connectivity/telematics/details/attachments.hbs +198 -121
  109. package/addon/templates/connectivity/telematics/details/devices.hbs +4 -1
  110. package/addon/templates/connectivity/telematics/details/events.hbs +4 -1
  111. package/addon/templates/connectivity/telematics/details/sensors.hbs +3 -0
  112. package/addon/templates/management/vehicles/index/edit.hbs +1 -1
  113. package/addon/templates/management/vendors/index/details.hbs +2 -2
  114. package/addon/templates/settings/notifications.hbs +2 -1
  115. package/addon/templates/settings/routing.hbs +1 -0
  116. package/addon/utils/device-table-columns.js +288 -0
  117. package/addon/utils/fleet-ops-options.js +38 -0
  118. package/addon/utils/identity-cell-resource.js +20 -0
  119. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  120. package/addon/utils/trackable-option.js +93 -0
  121. package/app/components/cell/attached-vehicle.js +1 -0
  122. package/app/components/cell/device-identity.js +1 -0
  123. package/app/components/cell/driver-identity.js +1 -0
  124. package/app/components/cell/equipment-identity.js +1 -0
  125. package/app/components/cell/part-identity.js +1 -0
  126. package/app/components/cell/telematic-device.js +1 -0
  127. package/app/components/cell/vehicle-identity.js +1 -0
  128. package/app/components/device/panel-tabs/events.hbs +30 -0
  129. package/app/components/device/panel-tabs/events.js +128 -0
  130. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  131. package/app/components/device/panel-tabs/sensors.js +93 -0
  132. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  133. package/app/components/device/panel-tabs/vehicle.js +90 -0
  134. package/app/components/order/form/orchestrator-constraints.js +1 -0
  135. package/app/components/table/cell/dropdown/action-item.js +1 -0
  136. package/app/components/table/cell/dropdown.js +1 -0
  137. package/app/components/vendor/panel-header.js +1 -0
  138. package/app/controllers/connectivity/devices/index/details/sensors.js +1 -0
  139. package/app/controllers/connectivity/devices/index/details/vehicle.js +1 -0
  140. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  141. package/app/routes/connectivity/devices/index/details/sensors.js +1 -0
  142. package/app/routes/connectivity/devices/index/details/vehicle.js +1 -0
  143. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  144. package/app/templates/connectivity/devices/index/details/sensors.js +1 -0
  145. package/app/templates/connectivity/devices/index/details/vehicle.js +1 -0
  146. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  147. package/app/utils/map-drawer-dropdown-position.js +1 -0
  148. package/app/utils/trackable-option.js +1 -0
  149. package/composer.json +1 -1
  150. package/extension.json +1 -1
  151. package/package.json +4 -4
  152. package/server/migrations/2025_08_28_054930_create_work_orders_table.php +1 -0
  153. package/server/migrations/2026_06_18_000001_add_category_to_work_orders_table.php +32 -0
  154. package/server/seeders/Testing/MaintenanceSeeder.php +5 -4
  155. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +1 -0
  156. package/server/src/Console/Commands/SyncTelematics.php +89 -0
  157. package/server/src/Exceptions/TelematicProviderException.php +13 -2
  158. package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -21
  159. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +55 -1
  160. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +17 -6
  161. package/server/src/Http/Controllers/Internal/v1/FleetController.php +9 -0
  162. package/server/src/Http/Controllers/Internal/v1/LiveController.php +178 -1
  163. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +1 -0
  164. package/server/src/Http/Controllers/Internal/v1/OrderController.php +4 -32
  165. package/server/src/Http/Controllers/Internal/v1/SearchController.php +2 -2
  166. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +4 -4
  167. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  168. package/server/src/Http/Filter/DeviceEventFilter.php +65 -0
  169. package/server/src/Http/Filter/DeviceFilter.php +80 -0
  170. package/server/src/Http/Filter/SensorFilter.php +125 -0
  171. package/server/src/Http/Filter/VehicleFilter.php +13 -0
  172. package/server/src/Http/Requests/CreateDriverRequest.php +14 -13
  173. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -0
  174. package/server/src/Http/Resources/v1/Driver.php +11 -2
  175. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  176. package/server/src/Http/Resources/v1/WorkOrder.php +1 -0
  177. package/server/src/Jobs/FinalizeApiOrderCreation.php +47 -0
  178. package/server/src/Jobs/FinalizeInternalOrderCreation.php +35 -0
  179. package/server/src/Jobs/NotifyBulkAssignedDriver.php +47 -0
  180. package/server/src/Jobs/SyncTelematicDevicesJob.php +180 -123
  181. package/server/src/Models/Device.php +17 -1
  182. package/server/src/Models/DeviceEvent.php +82 -1
  183. package/server/src/Models/Driver.php +16 -0
  184. package/server/src/Models/Equipment.php +55 -0
  185. package/server/src/Models/Sensor.php +15 -2
  186. package/server/src/Models/Telematic.php +1 -1
  187. package/server/src/Models/Vehicle.php +5 -1
  188. package/server/src/Models/WorkOrder.php +5 -2
  189. package/server/src/Observers/DriverObserver.php +3 -3
  190. package/server/src/Observers/FleetObserver.php +23 -0
  191. package/server/src/Observers/VehicleObserver.php +3 -3
  192. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  193. package/server/src/Support/LiveCacheService.php +1 -1
  194. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +143 -20
  195. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  196. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  197. package/server/src/Support/Telematics/Providers/SafeeProvider.php +70 -5
  198. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  199. package/server/src/Support/Telematics/TelematicService.php +330 -30
  200. package/server/src/routes.php +1 -1
  201. package/server/tests/DeviceFilterTest.php +45 -0
  202. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  203. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  204. package/server/tests/OperationsMonitorEndpointTest.php +38 -0
  205. package/server/tests/OrderQueuedClosureRegressionTest.php +36 -0
  206. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  207. package/server/tests/TelematicsHardeningTest.php +598 -6
  208. package/server/tests/VehicleDriverAssignmentTest.php +27 -0
  209. package/translations/en-us.yaml +1 -0
  210. package/addon/controllers/connectivity/events/index/details.js +0 -3
  211. package/addon/routes/connectivity/events/index/details.js +0 -3
  212. /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
@@ -1,3 +1,176 @@
1
1
  import Component from '@glimmer/component';
2
+ import { inject as service } from '@ember/service';
3
+ import { tracked } from '@glimmer/tracking';
4
+ import { task } from 'ember-concurrency';
2
5
 
3
- export default class DeviceDetailsComponent extends Component {}
6
+ const warningSeverities = ['warning', 'error', 'critical', 'high'];
7
+
8
+ export default class DeviceDetailsComponent extends Component {
9
+ @service store;
10
+ @service intl;
11
+
12
+ @tracked sensors = [];
13
+ @tracked events = [];
14
+ @tracked sensorTotal;
15
+ @tracked eventTotal;
16
+
17
+ constructor() {
18
+ super(...arguments);
19
+ this.loadOperationalSummary.perform();
20
+ }
21
+
22
+ get resource() {
23
+ return this.args.resource;
24
+ }
25
+
26
+ get displayName() {
27
+ return this.resource?.displayName ?? this.resource?.name ?? this.resource?.device_id ?? this.resource?.serial_number ?? this.resource?.public_id ?? this.intl.t('resource.device');
28
+ }
29
+
30
+ get providerDescriptor() {
31
+ return this.resource?.telematic?.provider_descriptor;
32
+ }
33
+
34
+ get providerLabel() {
35
+ return this.providerDescriptor?.label ?? this.resource?.telematic?.name ?? this.resource?.provider ?? this.resource?.telematic_name;
36
+ }
37
+
38
+ get providerIcon() {
39
+ return this.providerDescriptor?.icon;
40
+ }
41
+
42
+ get connectionStatus() {
43
+ return this.resource?.connection_status ?? (this.resource?.is_online ? 'online' : 'offline');
44
+ }
45
+
46
+ get connectionTone() {
47
+ switch (this.connectionStatus) {
48
+ case 'online':
49
+ return 'online';
50
+ case 'recently_offline':
51
+ return 'warning';
52
+ case 'never_connected':
53
+ case 'long_offline':
54
+ return 'danger';
55
+ default:
56
+ return 'offline';
57
+ }
58
+ }
59
+
60
+ get connectionLabel() {
61
+ return this.connectionStatus ? this.connectionStatus.replaceAll('_', ' ') : 'Unknown';
62
+ }
63
+
64
+ get attachedVehicleName() {
65
+ return this.resource?.attached_to_name ?? this.resource?.attachable?.displayName ?? this.resource?.attachable?.display_name ?? this.resource?.attachable?.name;
66
+ }
67
+
68
+ get attachmentLabel() {
69
+ return this.attachedVehicleName ?? 'Unattached';
70
+ }
71
+
72
+ get attachmentTone() {
73
+ return this.attachedVehicleName ? 'success' : 'warning';
74
+ }
75
+
76
+ get lastSeenLabel() {
77
+ return this.resource?.last_online_at ?? this.resource?.lastOnlineAt;
78
+ }
79
+
80
+ get signalLabel() {
81
+ return this.resource?.signal_strength ?? this.resource?.data?.signal_strength ?? this.resource?.meta?.signal_strength;
82
+ }
83
+
84
+ get firmwareLabel() {
85
+ return this.resource?.firmware_version ?? this.resource?.data?.firmware_version ?? this.resource?.meta?.firmware_version;
86
+ }
87
+
88
+ get lastPositionLabel() {
89
+ const position = this.resource?.last_position;
90
+
91
+ if (!position) {
92
+ return null;
93
+ }
94
+
95
+ if (typeof position === 'string') {
96
+ return position;
97
+ }
98
+
99
+ const latitude = position.latitude ?? position.lat ?? position.coordinates?.[1];
100
+ const longitude = position.longitude ?? position.lng ?? position.lon ?? position.coordinates?.[0];
101
+
102
+ if (latitude && longitude) {
103
+ return `${latitude}, ${longitude}`;
104
+ }
105
+
106
+ return null;
107
+ }
108
+
109
+ get warningEventsCount() {
110
+ return this.events.filter((event) => warningSeverities.includes(String(event.severity ?? '').toLowerCase())).length;
111
+ }
112
+
113
+ get unprocessedEventsCount() {
114
+ return this.events.filter((event) => !event.processed_at && !event.is_processed).length;
115
+ }
116
+
117
+ get activeSensorsCount() {
118
+ return this.sensors.filter((sensor) => sensor.is_active || sensor.status === 'active' || sensor.status === 'online').length;
119
+ }
120
+
121
+ get reportingSensorsCount() {
122
+ return this.sensors.filter((sensor) => sensor.last_value || sensor.last_reading_at).length;
123
+ }
124
+
125
+ get metrics() {
126
+ return [
127
+ {
128
+ label: 'Connection',
129
+ value: this.connectionLabel,
130
+ icon: this.connectionStatus === 'online' ? 'signal' : 'power-off',
131
+ accentClass: this.connectionStatus === 'online' ? 'fleetops-connectivity-kpi-accent-green' : 'fleetops-connectivity-kpi-accent-rose',
132
+ },
133
+ {
134
+ label: 'Sensors',
135
+ value: this.sensorTotal ?? this.sensors.length,
136
+ meta: `${this.activeSensorsCount} active`,
137
+ icon: 'gauge-high',
138
+ accentClass: 'fleetops-connectivity-kpi-accent-blue',
139
+ },
140
+ {
141
+ label: 'Events',
142
+ value: this.eventTotal ?? this.events.length,
143
+ meta: `${this.warningEventsCount + this.unprocessedEventsCount} need review`,
144
+ icon: 'bolt',
145
+ accentClass: this.warningEventsCount || this.unprocessedEventsCount ? 'fleetops-connectivity-kpi-accent-amber' : 'fleetops-connectivity-kpi-accent-green',
146
+ },
147
+ {
148
+ label: 'Vehicle',
149
+ value: this.attachmentLabel,
150
+ icon: this.attachedVehicleName ? 'truck' : 'link-slash',
151
+ accentClass: this.attachedVehicleName ? 'fleetops-connectivity-kpi-accent-green' : 'fleetops-connectivity-kpi-accent-amber',
152
+ },
153
+ ];
154
+ }
155
+
156
+ @task *loadOperationalSummary() {
157
+ if (!this.resource?.id) {
158
+ return;
159
+ }
160
+
161
+ try {
162
+ const [sensors, events] = yield Promise.all([
163
+ this.store.query('sensor', { device_uuid: this.resource.id, limit: 10 }),
164
+ this.store.query('device-event', { device_uuid: this.resource.id, limit: 10, sort: '-created_at' }),
165
+ ]);
166
+
167
+ this.sensors = Array.from(sensors ?? []);
168
+ this.events = Array.from(events ?? []);
169
+ this.sensorTotal = sensors?.meta?.total;
170
+ this.eventTotal = events?.meta?.total;
171
+ } catch {
172
+ this.sensors = [];
173
+ this.events = [];
174
+ }
175
+ }
176
+ }
@@ -10,7 +10,7 @@
10
10
  @infiniteScroll={{false}}
11
11
  @renderInPlace={{true}}
12
12
  @onChange={{this.selectTelematic}}
13
- @disabled={{cannot-write @resource}}
13
+ @disabled={{or (cannot-write @resource) this.isTelematicLocked}}
14
14
  as |model|
15
15
  >
16
16
  <div class="space-x-2 text-sm">
@@ -227,4 +227,4 @@
227
227
  <RegistryYield @registry="fleet-ops:component:device:form" as |RegistryComponent|>
228
228
  <RegistryComponent @resource={{@resource}} @controller={{@controller}} @permission={{get-write-permission @resource}} />
229
229
  </RegistryYield>
230
- </div>
230
+ </div>
@@ -8,6 +8,13 @@ export default class DeviceFormComponent extends Component {
8
8
  @service currentUser;
9
9
  @service notifications;
10
10
 
11
+ get isTelematicLocked() {
12
+ const resource = this.args.resource;
13
+ const hasTelematic = Boolean(resource?.telematic_uuid || resource?.telematic?.id);
14
+
15
+ return Boolean(hasTelematic && resource?.isNew === false);
16
+ }
17
+
11
18
  @action selectTelematic(telematic) {
12
19
  this.args.resource.setProperties({
13
20
  telematic,
@@ -1,35 +1,60 @@
1
- <div class="px-4 py-2">
2
- <div class="flex flex-1 flex-row items-start justify-between">
3
- <div class="flex flex-row space-x-3">
4
- <div class="flex items-start justify-start rounded-full">
1
+ <div class="px-4 py-3" ...attributes>
2
+ <div class="flex flex-1 flex-row items-start justify-between gap-4">
3
+ <div class="flex min-w-0 flex-row items-start gap-3">
4
+ <div class="relative flex flex-shrink-0 items-start justify-start">
5
5
  <Image
6
- src={{@resource.photo_url}}
6
+ src={{this.resource.photo_url}}
7
7
  @fallbackSrc={{config "defaultValues.placeholderImage"}}
8
- alt={{@resource.displayName}}
8
+ alt={{this.name}}
9
9
  height="48"
10
10
  width="48"
11
- class="h-14 w-14 rounded-full shadow-sm"
11
+ class="h-14 w-14 rounded-md border border-gray-200 object-cover shadow-sm dark:border-gray-700"
12
12
  />
13
13
  <Attach::Tooltip @class="clean" @animation="scale" @placement="top">
14
- <InputInfo @text={{@resource.displayName}} />
14
+ <InputInfo @text={{this.name}} />
15
15
  </Attach::Tooltip>
16
16
  </div>
17
- <div class="flex flex-col">
18
- <h1 class="text-gray-900 dark:text-white font-semibold text-lg">{{@resource.displayName}}</h1>
19
- <div>
20
- <div class="text-xs text-gray-400 dark:text-gray-500">{{concat
21
- (n-a (titleize @resource.provider))
22
- " "
23
- (n-a (get-fleet-ops-option-label "deviceTypes" @resource.type))
24
- }}</div>
25
- <div class="text-xs text-gray-400 dark:text-gray-500">{{n-a @resource.serial_number}}</div>
17
+ <div class="flex min-w-0 flex-col">
18
+ <div class="flex min-w-0 flex-wrap items-center gap-2">
19
+ <h1 class="truncate text-base font-semibold leading-5 text-gray-900 dark:text-gray-100">{{this.name}}</h1>
20
+ <Badge @status={{this.connectionStatus}} @size="xs">{{smart-humanize this.connectionStatus}}</Badge>
21
+ </div>
22
+
23
+ <div class="mt-1 flex max-w-2xl flex-wrap items-center gap-1.5">
24
+ {{#if this.provider}}
25
+ <span class="inline-flex max-w-[12rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
26
+ <FaIcon @icon="satellite-dish" class="text-gray-400 dark:text-gray-300" />
27
+ <span class="truncate">{{titleize this.provider}}</span>
28
+ </span>
29
+ {{/if}}
30
+ {{#if this.type}}
31
+ <span class="inline-flex max-w-[10rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
32
+ <FaIcon @icon="microchip" class="text-gray-400 dark:text-gray-300" />
33
+ <span class="truncate">{{n-a (get-fleet-ops-option-label "deviceTypes" this.type)}}</span>
34
+ </span>
35
+ {{/if}}
36
+ {{#if this.identifier}}
37
+ <span class="inline-flex max-w-[12rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
38
+ <FaIcon @icon="id-card" class="text-gray-400 dark:text-gray-300" />
39
+ <span class="truncate">{{this.identifier}}</span>
40
+ </span>
41
+ {{/if}}
42
+ {{#if this.attachedVehicle}}
43
+ <span class="inline-flex max-w-[12rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
44
+ <FaIcon @icon="car" class="text-gray-400 dark:text-gray-300" />
45
+ <span class="truncate">{{this.attachedVehicle}}</span>
46
+ </span>
47
+ {{/if}}
48
+ <span class="inline-flex max-w-[13rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
49
+ <FaIcon @icon="clock" class="text-gray-400 dark:text-gray-300" />
50
+ <span class="truncate">{{#if this.lastOnlineAt}}{{format-date-fns this.lastOnlineAt "dd MMM yyyy, HH:mm"}}{{else}}No last online{{/if}}</span>
51
+ </span>
26
52
  </div>
27
- <Badge @status={{if @resource.online "online" "offline"}}>{{if @resource.online (t "common.online") (t "common.offline")}}</Badge>
28
53
  </div>
29
54
  </div>
30
- <div class="next-view-header-right">
55
+ <div class="next-view-header-right flex-shrink-0">
31
56
  <Layout::Resource::Panel::HeaderActions
32
- @resource={{@resource}}
57
+ @resource={{this.resource}}
33
58
  @saveTask={{@saveTask}}
34
59
  @saveOptions={{@saveOptions}}
35
60
  @saveDisabled={{@saveDisabled}}
@@ -40,4 +65,4 @@
40
65
  />
41
66
  </div>
42
67
  </div>
43
- </div>
68
+ </div>
@@ -1,3 +1,51 @@
1
1
  import Component from '@glimmer/component';
2
+ import { get } from '@ember/object';
2
3
 
3
- export default class DevicePanelHeaderComponent extends Component {}
4
+ export default class DevicePanelHeaderComponent extends Component {
5
+ get resource() {
6
+ return this.args.resource ?? {};
7
+ }
8
+
9
+ get name() {
10
+ return (
11
+ get(this.resource, 'displayName') ??
12
+ get(this.resource, 'display_name') ??
13
+ get(this.resource, 'name') ??
14
+ get(this.resource, 'device_id') ??
15
+ get(this.resource, 'imei') ??
16
+ get(this.resource, 'serial_number') ??
17
+ get(this.resource, 'public_id') ??
18
+ '-'
19
+ );
20
+ }
21
+
22
+ get connectionStatus() {
23
+ return get(this.resource, 'connection_status') ?? get(this.resource, 'status') ?? (get(this.resource, 'is_online') || get(this.resource, 'online') ? 'online' : 'offline');
24
+ }
25
+
26
+ get provider() {
27
+ return get(this.resource, 'telematic.name') ?? get(this.resource, 'telematic_name') ?? get(this.resource, 'provider');
28
+ }
29
+
30
+ get type() {
31
+ return get(this.resource, 'type');
32
+ }
33
+
34
+ get identifier() {
35
+ return get(this.resource, 'imei') ?? get(this.resource, 'serial_number') ?? get(this.resource, 'device_id') ?? get(this.resource, 'public_id');
36
+ }
37
+
38
+ get attachedVehicle() {
39
+ return (
40
+ get(this.resource, 'attached_to_name') ??
41
+ get(this.resource, 'attachable.displayName') ??
42
+ get(this.resource, 'attachable.display_name') ??
43
+ get(this.resource, 'attachable.name') ??
44
+ get(this.resource, 'attachable_uuid')
45
+ );
46
+ }
47
+
48
+ get lastOnlineAt() {
49
+ return get(this.resource, 'last_online_at') ?? get(this.resource, 'lastOnlineAt');
50
+ }
51
+ }
@@ -0,0 +1,30 @@
1
+ <div class="flex flex-col gap-3 p-3" ...attributes>
2
+ <div class="flex items-start justify-between gap-3">
3
+ <div class="min-w-0">
4
+ <h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Telemetry Events</h2>
5
+ <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">Recent device events, warning signals, and processing state.</p>
6
+ </div>
7
+ <Button @icon="refresh" @size="xs" @onClick={{this.refreshEvents}} @isLoading={{this.loadEvents.isRunning}} />
8
+ </div>
9
+
10
+ <Layout::Resource::Tabular
11
+ @resource="device-event"
12
+ @title="Telemetry Events"
13
+ @data={{this.events}}
14
+ @columns={{this.columns}}
15
+ @controller={{this}}
16
+ @pagination={{false}}
17
+ @withoutHeader={{true}}
18
+ @tableWrapperClass="no-table-extra-spacing"
19
+ @emptyStateComponent={{component
20
+ "table/empty-state"
21
+ variant="compact"
22
+ icon="bolt"
23
+ title="No telemetry events"
24
+ description="Events appear after this device reports provider activity."
25
+ primaryText="Refresh"
26
+ primaryIcon="refresh"
27
+ primaryAction=this.refreshEvents
28
+ }}
29
+ />
30
+ </div>
@@ -0,0 +1,128 @@
1
+ import Component from '@glimmer/component';
2
+ import { action } from '@ember/object';
3
+ import { inject as service } from '@ember/service';
4
+ import { tracked } from '@glimmer/tracking';
5
+ import { task } from 'ember-concurrency';
6
+
7
+ const severityOptions = [
8
+ { label: 'Info', value: 'info' },
9
+ { label: 'Warning', value: 'warning' },
10
+ { label: 'Error', value: 'error' },
11
+ { label: 'Critical', value: 'critical' },
12
+ { label: 'High', value: 'high' },
13
+ ];
14
+
15
+ function toRecentList(records) {
16
+ const list = Array.from(records ?? []);
17
+
18
+ list.meta = {
19
+ current_page: 1,
20
+ last_page: 1,
21
+ per_page: list.length,
22
+ total: list.length,
23
+ from: list.length > 0 ? 1 : 0,
24
+ to: list.length,
25
+ };
26
+
27
+ return list;
28
+ }
29
+
30
+ export default class DevicePanelTabsEventsComponent extends Component {
31
+ @service deviceEventActions;
32
+ @service store;
33
+
34
+ @tracked events = toRecentList();
35
+
36
+ constructor() {
37
+ super(...arguments);
38
+ this.loadEvents.perform();
39
+ }
40
+
41
+ get device() {
42
+ return this.args.resource ?? this.args.model;
43
+ }
44
+
45
+ get columns() {
46
+ return [
47
+ {
48
+ sticky: true,
49
+ label: 'Event',
50
+ valuePath: 'event_type',
51
+ cellComponent: 'table/cell/anchor',
52
+ action: this.deviceEventActions.panel?.view ?? this.deviceEventActions.transition.view,
53
+ permission: 'fleet-ops view device-event',
54
+ resizable: true,
55
+ },
56
+ {
57
+ label: 'Severity',
58
+ valuePath: 'severity',
59
+ cellComponent: 'table/cell/status',
60
+ filterOptions: severityOptions,
61
+ resizable: true,
62
+ },
63
+ {
64
+ label: 'Message',
65
+ valuePath: 'message',
66
+ resizable: true,
67
+ },
68
+ {
69
+ label: 'Code',
70
+ valuePath: 'code',
71
+ resizable: true,
72
+ },
73
+ {
74
+ label: 'Processed',
75
+ valuePath: 'processedAt',
76
+ sortParam: 'processed_at',
77
+ resizable: true,
78
+ },
79
+ {
80
+ label: 'Occurred',
81
+ valuePath: 'occurredAt',
82
+ sortParam: 'occurred_at',
83
+ resizable: true,
84
+ },
85
+ {
86
+ label: '',
87
+ cellComponent: 'table/cell/dropdown',
88
+ ddButtonText: false,
89
+ ddButtonIcon: 'ellipsis-h',
90
+ ddButtonIconPrefix: 'fas',
91
+ wrapperClass: 'flex items-center justify-end mx-2',
92
+ sticky: 'right',
93
+ width: 60,
94
+ actions: [
95
+ {
96
+ label: 'View event',
97
+ fn: this.deviceEventActions.panel?.view ?? this.deviceEventActions.transition.view,
98
+ permission: 'fleet-ops view device-event',
99
+ },
100
+ {
101
+ label: 'Mark processed',
102
+ fn: this.markProcessed,
103
+ permission: 'fleet-ops update device-event',
104
+ },
105
+ ],
106
+ },
107
+ ];
108
+ }
109
+
110
+ @action async markProcessed(deviceEvent) {
111
+ await this.deviceEventActions.markProcessed(deviceEvent);
112
+ await this.loadEvents.perform();
113
+ }
114
+
115
+ @action refreshEvents() {
116
+ return this.loadEvents.perform();
117
+ }
118
+
119
+ @task *loadEvents() {
120
+ if (!this.device?.id) {
121
+ this.events = toRecentList();
122
+ return;
123
+ }
124
+
125
+ const events = yield this.store.query('device-event', { device_uuid: this.device.id, limit: 10, sort: '-created_at' });
126
+ this.events = toRecentList(events);
127
+ }
128
+ }
@@ -0,0 +1,30 @@
1
+ <div class="flex flex-col gap-3 p-3" ...attributes>
2
+ <div class="flex items-start justify-between gap-3">
3
+ <div class="min-w-0">
4
+ <h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Sensor Inventory</h2>
5
+ <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">Recent sensor readings and health for this device.</p>
6
+ </div>
7
+ <Button @icon="refresh" @size="xs" @onClick={{this.refreshSensors}} @isLoading={{this.loadSensors.isRunning}} />
8
+ </div>
9
+
10
+ <Layout::Resource::Tabular
11
+ @resource="sensor"
12
+ @title="Device Sensors"
13
+ @data={{this.sensors}}
14
+ @columns={{this.columns}}
15
+ @controller={{this}}
16
+ @pagination={{false}}
17
+ @withoutHeader={{true}}
18
+ @tableWrapperClass="no-table-extra-spacing"
19
+ @emptyStateComponent={{component
20
+ "table/empty-state"
21
+ variant="compact"
22
+ icon="gauge-high"
23
+ title="No sensors connected"
24
+ description="Sensors appear here after provider telemetry links them to this device."
25
+ primaryText="Refresh"
26
+ primaryIcon="refresh"
27
+ primaryAction=this.refreshSensors
28
+ }}
29
+ />
30
+ </div>
@@ -0,0 +1,93 @@
1
+ import Component from '@glimmer/component';
2
+ import { action } from '@ember/object';
3
+ import { inject as service } from '@ember/service';
4
+ import { tracked } from '@glimmer/tracking';
5
+ import { task } from 'ember-concurrency';
6
+
7
+ function toRecentList(records) {
8
+ const list = Array.from(records ?? []);
9
+
10
+ list.meta = {
11
+ current_page: 1,
12
+ last_page: 1,
13
+ per_page: list.length,
14
+ total: list.length,
15
+ from: list.length > 0 ? 1 : 0,
16
+ to: list.length,
17
+ };
18
+
19
+ return list;
20
+ }
21
+
22
+ export default class DevicePanelTabsSensorsComponent extends Component {
23
+ @service sensorActions;
24
+ @service store;
25
+
26
+ @tracked sensors = toRecentList();
27
+
28
+ constructor() {
29
+ super(...arguments);
30
+ this.loadSensors.perform();
31
+ }
32
+
33
+ get device() {
34
+ return this.args.resource ?? this.args.model;
35
+ }
36
+
37
+ get columns() {
38
+ return [
39
+ {
40
+ sticky: true,
41
+ label: 'Sensor',
42
+ valuePath: 'name',
43
+ cellComponent: 'table/cell/anchor',
44
+ action: this.sensorActions.panel?.view ?? this.sensorActions.transition.view,
45
+ permission: 'fleet-ops view sensor',
46
+ resizable: true,
47
+ },
48
+ {
49
+ label: 'Type',
50
+ valuePath: 'type',
51
+ cellComponent: 'table/cell/base',
52
+ humanize: true,
53
+ resizable: true,
54
+ },
55
+ {
56
+ label: 'Value',
57
+ valuePath: 'last_value',
58
+ resizable: true,
59
+ },
60
+ {
61
+ label: 'Unit',
62
+ valuePath: 'unit',
63
+ resizable: true,
64
+ },
65
+ {
66
+ label: 'Status',
67
+ valuePath: 'status',
68
+ cellComponent: 'table/cell/status',
69
+ resizable: true,
70
+ },
71
+ {
72
+ label: 'Last Reading',
73
+ valuePath: 'lastReadingAt',
74
+ sortParam: 'last_reading_at',
75
+ resizable: true,
76
+ },
77
+ ];
78
+ }
79
+
80
+ @action refreshSensors() {
81
+ return this.loadSensors.perform();
82
+ }
83
+
84
+ @task *loadSensors() {
85
+ if (!this.device?.id) {
86
+ this.sensors = toRecentList();
87
+ return;
88
+ }
89
+
90
+ const sensors = yield this.store.query('sensor', { device_uuid: this.device.id, limit: 10, sort: '-updated_at' });
91
+ this.sensors = toRecentList(sensors);
92
+ }
93
+ }