@fleetbase/fleetops-engine 0.6.20 → 0.6.22

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 (162) hide show
  1. package/addon/components/custom-entity/form.hbs +14 -14
  2. package/addon/components/device/card.hbs +1 -0
  3. package/addon/components/device/card.js +3 -0
  4. package/addon/components/device/details.hbs +92 -43
  5. package/addon/components/device/form.hbs +108 -60
  6. package/addon/components/device/form.js +36 -8
  7. package/addon/components/device/manager.hbs +29 -0
  8. package/addon/components/device/manager.js +95 -0
  9. package/addon/components/device/panel-header.hbs +32 -0
  10. package/addon/components/device/panel-header.js +3 -0
  11. package/addon/components/device/pill.hbs +16 -0
  12. package/addon/components/device/pill.js +3 -0
  13. package/addon/components/driver/details.hbs +4 -0
  14. package/addon/components/driver/details.js +19 -1
  15. package/addon/components/driver/form.hbs +14 -3
  16. package/addon/components/driver/form.js +49 -47
  17. package/addon/components/driver/pill.hbs +17 -0
  18. package/addon/components/driver/pill.js +3 -0
  19. package/addon/components/entity/form.hbs +7 -5
  20. package/addon/components/layout/fleet-ops-sidebar.js +12 -12
  21. package/addon/components/map/drawer/device-event-listing.hbs +64 -0
  22. package/addon/components/map/drawer/device-event-listing.js +181 -0
  23. package/addon/components/map/drawer/position-listing.hbs +100 -0
  24. package/addon/components/map/drawer/position-listing.js +455 -0
  25. package/addon/components/map/drawer.js +2 -0
  26. package/addon/components/map/leaflet-live-map.hbs +7 -2
  27. package/addon/components/modals/attach-device.hbs +18 -0
  28. package/addon/components/modals/attach-device.js +3 -0
  29. package/addon/components/order/details/detail.hbs +2 -54
  30. package/addon/components/order/details/detail.js +1 -0
  31. package/addon/components/order/details/payload.hbs +6 -4
  32. package/addon/components/order/details/payload.js +2 -0
  33. package/addon/components/order/pill.hbs +34 -0
  34. package/addon/components/order/pill.js +3 -0
  35. package/addon/components/order-config-manager/custom-fields.js +1 -1
  36. package/addon/components/positions-replay.hbs +339 -0
  37. package/addon/components/positions-replay.js +409 -0
  38. package/addon/components/sensor/details.hbs +64 -38
  39. package/addon/components/sensor/form.hbs +112 -63
  40. package/addon/components/sensor/form.js +36 -24
  41. package/addon/components/sensor/panel-header.hbs +32 -0
  42. package/addon/components/sensor/panel-header.js +3 -0
  43. package/addon/components/telematic/details.hbs +40 -16
  44. package/addon/components/telematic/form.hbs +63 -64
  45. package/addon/components/telematic/form.js +73 -4
  46. package/addon/components/vehicle/card.hbs +2 -2
  47. package/addon/components/vehicle/details.hbs +4 -0
  48. package/addon/components/vehicle/details.js +19 -1
  49. package/addon/components/vehicle/form.hbs +4 -0
  50. package/addon/components/vehicle/pill.hbs +34 -0
  51. package/addon/components/vehicle/pill.js +3 -0
  52. package/addon/controllers/analytics/reports/index/edit.js +1 -1
  53. package/addon/controllers/connectivity/devices/index/details.js +22 -1
  54. package/addon/controllers/connectivity/devices/index/edit.js +66 -1
  55. package/addon/controllers/connectivity/devices/index.js +51 -9
  56. package/addon/controllers/connectivity/events/index.js +65 -16
  57. package/addon/controllers/connectivity/sensors/index/details.js +22 -1
  58. package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
  59. package/addon/controllers/connectivity/sensors/index.js +66 -6
  60. package/addon/controllers/connectivity/telematics/index/details.js +22 -1
  61. package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
  62. package/addon/controllers/connectivity/telematics/index.js +20 -11
  63. package/addon/controllers/management/fleets/index/details.js +26 -21
  64. package/addon/controllers/management/fleets/index/edit.js +9 -6
  65. package/addon/controllers/management/vehicles/index/details.js +26 -13
  66. package/addon/controllers/settings/custom-fields.js +6 -0
  67. package/addon/helpers/get-fleet-ops-option-label.js +11 -0
  68. package/addon/routes/connectivity/devices/index/details.js +27 -1
  69. package/addon/routes/connectivity/devices/index/edit.js +27 -1
  70. package/addon/routes/connectivity/sensors/index/details.js +27 -1
  71. package/addon/routes/connectivity/sensors/index/edit.js +27 -1
  72. package/addon/routes/connectivity/telematics/index/details.js +27 -1
  73. package/addon/routes/connectivity/telematics/index/edit.js +27 -1
  74. package/addon/routes/management/drivers/index/details/positions.js +3 -0
  75. package/addon/routes/management/vehicles/index/details/positions.js +3 -0
  76. package/addon/routes.js +4 -0
  77. package/addon/services/movement-tracker.js +81 -30
  78. package/addon/services/position-playback.js +486 -0
  79. package/addon/services/resource-metadata.js +46 -0
  80. package/addon/styles/fleetops-engine.css +157 -0
  81. package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
  82. package/addon/templates/connectivity/devices/index/details.hbs +15 -2
  83. package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
  84. package/addon/templates/connectivity/events/index.hbs +1 -1
  85. package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
  86. package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
  87. package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
  88. package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
  89. package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
  90. package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
  91. package/addon/templates/management/drivers/index/details/positions.hbs +2 -0
  92. package/addon/templates/management/vehicles/index/details/devices.hbs +1 -2
  93. package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
  94. package/addon/utils/fleet-ops-options.js +95 -0
  95. package/app/components/device/card.js +1 -0
  96. package/app/components/device/manager.js +1 -0
  97. package/app/components/device/panel-header.js +1 -0
  98. package/app/components/device/pill.js +1 -0
  99. package/app/components/driver/pill.js +1 -0
  100. package/app/components/map/drawer/device-event-listing.js +1 -0
  101. package/app/components/map/drawer/position-listing.js +1 -0
  102. package/app/components/modals/attach-device.js +1 -0
  103. package/app/components/order/pill.js +1 -0
  104. package/app/components/positions-replay.js +1 -0
  105. package/app/components/sensor/panel-header.js +1 -0
  106. package/app/components/vehicle/pill.js +1 -0
  107. package/app/helpers/get-fleet-ops-option-label.js +1 -0
  108. package/app/routes/management/drivers/index/details/positions.js +1 -0
  109. package/app/routes/management/vehicles/index/details/positions.js +1 -0
  110. package/app/services/position-playback.js +1 -0
  111. package/app/services/resource-metadata.js +1 -0
  112. package/app/templates/management/drivers/index/details/positions.js +1 -0
  113. package/app/templates/management/vehicles/index/details/positions.js +1 -0
  114. package/composer.json +1 -1
  115. package/extension.json +1 -1
  116. package/package.json +4 -4
  117. package/server/config/telematics.php +111 -0
  118. package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
  119. package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
  120. package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
  121. package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
  122. package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
  123. package/server/src/Contracts/TelematicProviderInterface.php +119 -0
  124. package/server/src/Exceptions/TelematicProviderException.php +14 -0
  125. package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
  126. package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
  127. package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
  128. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
  129. package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
  130. package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
  131. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
  132. package/server/src/Http/Controllers/TelematicWebhookController.php +169 -0
  133. package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
  134. package/server/src/Http/Filter/PositionFilter.php +35 -0
  135. package/server/src/Http/Resources/v1/Position.php +44 -0
  136. package/server/src/Jobs/ReplayPositions.php +64 -0
  137. package/server/src/Jobs/SendPositionReplay.php +65 -0
  138. package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
  139. package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
  140. package/server/src/Models/Asset.php +10 -8
  141. package/server/src/Models/Device.php +79 -12
  142. package/server/src/Models/DeviceEvent.php +33 -3
  143. package/server/src/Models/Driver.php +28 -1
  144. package/server/src/Models/Maintenance.php +15 -12
  145. package/server/src/Models/Part.php +2 -0
  146. package/server/src/Models/Payload.php +0 -1
  147. package/server/src/Models/Place.php +4 -1
  148. package/server/src/Models/Position.php +27 -17
  149. package/server/src/Models/Sensor.php +78 -13
  150. package/server/src/Models/Telematic.php +116 -6
  151. package/server/src/Models/TrackingNumber.php +3 -1
  152. package/server/src/Models/Vehicle.php +8 -11
  153. package/server/src/Models/WorkOrder.php +8 -5
  154. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  155. package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
  156. package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
  157. package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
  158. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
  159. package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
  160. package/server/src/Support/Telematics/TelematicService.php +223 -0
  161. package/server/src/Support/Utils.php +1 -1
  162. package/server/src/routes.php +24 -1
@@ -0,0 +1,409 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { action, set } from '@ember/object';
5
+ import { task } from 'ember-concurrency';
6
+ import { isArray } from '@ember/array';
7
+ import { guidFor } from '@ember/object/internals';
8
+ import { htmlSafe } from '@ember/template';
9
+ import { startOfWeek, endOfWeek, format } from 'date-fns';
10
+ import getModelName from '@fleetbase/ember-core/utils/get-model-name';
11
+
12
+ export default class PositionsReplayComponent extends Component {
13
+ @service store;
14
+ @service fetch;
15
+ @service positionPlayback;
16
+ @service notifications;
17
+ @service location;
18
+
19
+ /** Component ID */
20
+ id = guidFor(this);
21
+
22
+ /** Tracked properties - only what's NOT managed by service */
23
+ @tracked positions = [];
24
+ @tracked selectedOrder = null;
25
+ @tracked dateFilter = [format(startOfWeek(new Date(), { weekStartsOn: 1 }), 'yyyy-MM-dd'), format(endOfWeek(new Date(), { weekStartsOn: 1 }), 'yyyy-MM-dd')];
26
+ @tracked map = null;
27
+ @tracked replaySpeed = '1';
28
+ @tracked metrics = null;
29
+ @tracked latitude = this.args.resource.latitude || this.location.getLatitude();
30
+ @tracked longitude = this.args.resource.longitude || this.location.getLongitude();
31
+ @tracked zoom = 14;
32
+ @tracked tileUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
33
+
34
+ /** Computed properties - read state from service */
35
+ get isReplaying() {
36
+ return this.positionPlayback.isPlaying;
37
+ }
38
+
39
+ get isPaused() {
40
+ return this.positionPlayback.isPaused;
41
+ }
42
+
43
+ get currentReplayIndex() {
44
+ return this.positionPlayback.currentIndex;
45
+ }
46
+
47
+ get replayProgressWidth() {
48
+ return htmlSafe(`width: ${this.replayProgress}%;`);
49
+ }
50
+
51
+ get orderFilters() {
52
+ const params = {};
53
+
54
+ if (this.resourceType === 'vehicle') {
55
+ params.vehicle_assigned_uuid = this.resource?.id;
56
+ }
57
+
58
+ if (this.resourceType === 'driver') {
59
+ params.driver_assigned_uuid = this.resource?.id;
60
+ }
61
+
62
+ return params;
63
+ }
64
+
65
+ get resource() {
66
+ return this.args.resource;
67
+ }
68
+
69
+ get resourceName() {
70
+ if (!this.resource) {
71
+ return 'Unknown';
72
+ }
73
+ return this.resource.name || this.resource.display_name || this.resource.displayName || this.resource.public_id || 'Resource';
74
+ }
75
+
76
+ get resourceType() {
77
+ if (!this.resource) {
78
+ return 'resource';
79
+ }
80
+ return getModelName(this.resource) || 'resource';
81
+ }
82
+
83
+ get hasPositions() {
84
+ return this.positions.length > 0;
85
+ }
86
+
87
+ get totalPositions() {
88
+ return this.positions.length;
89
+ }
90
+
91
+ get replayProgress() {
92
+ if (this.totalPositions === 0) {
93
+ return 0;
94
+ }
95
+ return Math.round((this.currentReplayIndex / this.totalPositions) * 100);
96
+ }
97
+
98
+ get firstPosition() {
99
+ return this.positions.length > 0 ? this.positions[0] : null;
100
+ }
101
+
102
+ get lastPosition() {
103
+ return this.positions.length > 0 ? this.positions[this.positions.length - 1] : null;
104
+ }
105
+
106
+ get totalDistance() {
107
+ return this.metrics?.total_distance ?? 0;
108
+ }
109
+
110
+ get totalDuration() {
111
+ return this.metrics?.total_duration ?? 0;
112
+ }
113
+
114
+ get maxSpeed() {
115
+ return this.metrics?.max_speed ?? 0;
116
+ }
117
+
118
+ get avgSpeed() {
119
+ return this.metrics?.avg_speed ?? 0;
120
+ }
121
+
122
+ get speedingCount() {
123
+ return this.metrics?.speeding_count ?? 0;
124
+ }
125
+
126
+ get dwellCount() {
127
+ return this.metrics?.dwell_count ?? 0;
128
+ }
129
+
130
+ get accelerationCount() {
131
+ return this.metrics?.acceleration_count ?? 0;
132
+ }
133
+
134
+ get formattedDuration() {
135
+ const seconds = this.totalDuration;
136
+ const days = Math.floor(seconds / 86400);
137
+ const hours = Math.floor((seconds % 86400) / 3600);
138
+ const minutes = Math.floor((seconds % 3600) / 60);
139
+ const secs = seconds % 60;
140
+
141
+ if (days > 0) {
142
+ return `${days}d ${hours}h ${minutes}m ${secs}s`;
143
+ } else if (hours > 0) {
144
+ return `${hours}h ${minutes}m ${secs}s`;
145
+ } else if (minutes > 0) {
146
+ return `${minutes}m ${secs}s`;
147
+ } else {
148
+ return `${secs}s`;
149
+ }
150
+ }
151
+
152
+ /** Constants */
153
+ speedOptions = [
154
+ { label: '0.5x', value: '0.5' },
155
+ { label: '1x', value: '1' },
156
+ { label: '2x', value: '2' },
157
+ { label: '5x', value: '5' },
158
+ { label: '10x', value: '10' },
159
+ { label: '20x', value: '20' },
160
+ { label: '30x', value: '30' },
161
+ { label: '40x', value: '40' },
162
+ { label: '50x', value: '50' },
163
+ { label: '100x', value: '100' },
164
+ { label: '80x', value: '80' },
165
+ { label: '120x', value: '120' },
166
+ { label: '160x', value: '160' },
167
+ { label: '180x', value: '180' },
168
+ { label: '200x', value: '200' },
169
+ { label: '250x', value: '250' },
170
+ { label: '280x', value: '280' },
171
+ { label: '300x', value: '300' },
172
+ { label: '350x', value: '350' },
173
+ { label: '400x', value: '400' },
174
+ { label: '500x', value: '500' },
175
+ { label: '600x', value: '600' },
176
+ { label: '1000x', value: '1000' },
177
+ ];
178
+
179
+ /** Lifecycle */
180
+ constructor() {
181
+ super(...arguments);
182
+
183
+ // Validate resource argument
184
+ if (!this.args.resource) {
185
+ console.warn('PositionsReplay: @resource argument is required');
186
+ }
187
+
188
+ this.loadPositions.perform();
189
+ }
190
+
191
+ willDestroy() {
192
+ super.willDestroy?.();
193
+
194
+ // Clean up replay tracker on component destroy
195
+ this.positionPlayback.reset();
196
+ }
197
+
198
+ /** Actions */
199
+ @action didLoadMap({ target: map }) {
200
+ this.map = map;
201
+ requestAnimationFrame(() => map.invalidateSize());
202
+
203
+ const hasValidCoordinates = this.args.resource?.hasValidCoordinates || (Number.isFinite(this.args.resource?.latitude) && Number.isFinite(this.args.resource?.longitude));
204
+ if (hasValidCoordinates) {
205
+ const coordinates = [this.args.resource.latitude, this.args.resource.longitude];
206
+
207
+ // Use flyTo with a zoom level of 16 for a smooth animation
208
+ this.map.flyTo(coordinates, 16, {
209
+ animate: true,
210
+ duration: 0.8,
211
+ });
212
+ }
213
+ }
214
+
215
+ @action onOrderSelected(order) {
216
+ this.selectedOrder = order;
217
+ this.loadPositions.perform();
218
+ }
219
+
220
+ @action onDateRangeChanged({ formattedDate }) {
221
+ if (isArray(formattedDate) && formattedDate.length === 2) {
222
+ this.dateFilter = formattedDate;
223
+ this.loadPositions.perform();
224
+ }
225
+ }
226
+
227
+ @action onSpeedChanged(speed) {
228
+ this.replaySpeed = speed;
229
+
230
+ // Update replay speed in real-time if currently playing
231
+ if (this.isReplaying) {
232
+ this.positionPlayback.setSpeed(parseFloat(speed));
233
+ }
234
+ }
235
+
236
+ @action startReplay() {
237
+ if (this.positions.length === 0) {
238
+ this.notifications.warning('No positions to replay');
239
+ return;
240
+ }
241
+
242
+ if (this.isReplaying && !this.isPaused) {
243
+ this.notifications.info('Replay is already running');
244
+ return;
245
+ }
246
+
247
+ // If paused, resume
248
+ if (this.isPaused) {
249
+ this.positionPlayback.play();
250
+ return;
251
+ }
252
+
253
+ // Start new replay
254
+ this.#initializeReplay();
255
+ this.positionPlayback.play();
256
+ }
257
+
258
+ @action pauseReplay() {
259
+ if (!this.isReplaying) {
260
+ return;
261
+ }
262
+
263
+ this.positionPlayback.pause();
264
+ }
265
+
266
+ @action stopReplay() {
267
+ this.positionPlayback.stop();
268
+ }
269
+
270
+ @action stepForward() {
271
+ if (this.isReplaying) {
272
+ this.pauseReplay();
273
+ }
274
+ this.positionPlayback.stepForward(1);
275
+ }
276
+
277
+ @action stepBackward() {
278
+ if (this.isReplaying) {
279
+ this.pauseReplay();
280
+ }
281
+ this.positionPlayback.stepBackward(1);
282
+ }
283
+
284
+ @action clearFilters() {
285
+ this.selectedOrder = null;
286
+ this.dateFilter = null;
287
+ this.loadPositions.perform();
288
+ }
289
+
290
+ @action onPositionClicked(position) {
291
+ if (this.map && position.latitude && position.longitude) {
292
+ this.map.setView([position.latitude, position.longitude], this.zoom);
293
+ }
294
+ }
295
+
296
+ @action onTrackingMarkerAdded(resource, { target: layer }) {
297
+ this.#setResourceLayer(resource, layer);
298
+ }
299
+
300
+ /** Tasks */
301
+ @task *loadPositions() {
302
+ if (!this.args.resource) {
303
+ this.notifications.warning('No resource provided for position query');
304
+ return;
305
+ }
306
+
307
+ try {
308
+ const params = {
309
+ limit: 900,
310
+ sort: 'created_at',
311
+ subject_uuid: this.args.resource.id,
312
+ };
313
+
314
+ if (this.selectedOrder) {
315
+ params.order_uuid = this.selectedOrder.id;
316
+ }
317
+
318
+ if (isArray(this.dateFilter) && this.dateFilter.length === 2) {
319
+ params.created_at = this.dateFilter.join(',');
320
+ }
321
+
322
+ const positions = yield this.store.query('position', params);
323
+ this.positions = isArray(positions) ? positions : [];
324
+
325
+ if (this.positions?.length) {
326
+ yield this.loadMetrics.perform();
327
+
328
+ const bounds = positions
329
+ .filter(({ latitude, longitude }) => this.#isValidLatLng(latitude, longitude))
330
+ .map((pos) => pos.latLng)
331
+ .filter(Boolean);
332
+ const lastFiveBounds = bounds.slice(-5);
333
+ this.map.flyToBounds(lastFiveBounds, {
334
+ animate: true,
335
+ zoom: 16,
336
+ });
337
+ }
338
+
339
+ // Reset replay state when positions change
340
+ this.stopReplay();
341
+ } catch (error) {
342
+ this.notifications.serverError(error);
343
+ }
344
+ }
345
+
346
+ @task *loadMetrics() {
347
+ try {
348
+ const positionIds = this.positions.map((p) => p.id);
349
+
350
+ if (positionIds.length === 0) {
351
+ return;
352
+ }
353
+
354
+ const response = yield this.fetch.post('positions/metrics', {
355
+ position_ids: positionIds,
356
+ });
357
+
358
+ if (response && response.metrics) {
359
+ this.metrics = response.metrics;
360
+ }
361
+ } catch (error) {
362
+ this.notifications.serverError(error);
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Initialize replay tracker with current positions and settings
368
+ * This replaces the socket-based backend replay approach
369
+ *
370
+ * @private
371
+ */
372
+ #initializeReplay() {
373
+ if (!this.args.resource) {
374
+ this.notifications.warning('No resource provided for replay');
375
+ return;
376
+ }
377
+
378
+ if (this.positions.length === 0) {
379
+ this.notifications.warning('No positions to replay');
380
+ return;
381
+ }
382
+
383
+ // Initialize replay tracker with positions
384
+ this.positionPlayback.initialize({
385
+ subject: this.resource,
386
+ positions: this.positions,
387
+ speed: parseFloat(this.replaySpeed),
388
+ map: this.map,
389
+ callback: (data) => {
390
+ if (data.type === 'complete') {
391
+ // Replay completed
392
+ this.notifications.success('Replay completed');
393
+ }
394
+ },
395
+ });
396
+ }
397
+
398
+ #setResourceLayer(model, layer) {
399
+ const type = getModelName(model);
400
+
401
+ set(model, 'leafletLayer', layer);
402
+ set(layer, 'record_id', model.id);
403
+ set(layer, 'record_type', type);
404
+ }
405
+
406
+ #isValidLatLng(lat, lng) {
407
+ return Number.isFinite(lat) && Number.isFinite(lng) && lat <= 90 && lat >= -90 && lng <= 180 && lng >= -180 && lat !== 0 && lng !== 0;
408
+ }
409
+ }
@@ -1,28 +1,38 @@
1
1
  <div class="details-wrapper" ...attributes>
2
- <ContentPanel @title="Details" @open={{true}} @wrapperClass="bordered-top">
2
+ <ContentPanel @title="Identity" @open={{true}} @wrapperClass="bordered-top">
3
3
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
4
- <div class="field-info-container">
4
+ <div class="field-info-container ">
5
5
  <div class="field-name">Name</div>
6
6
  <div class="field-value">{{n-a @resource.name}}</div>
7
7
  </div>
8
8
 
9
9
  <div class="field-info-container">
10
10
  <div class="field-name">Sensor Type</div>
11
- <div class="field-value">{{smart-humanize @resource.sensor_type}}</div>
11
+ <div class="field-value">{{n-a (get-fleet-ops-option-label "sensorTypes" @resource.type)}}</div>
12
12
  </div>
13
13
 
14
+ <div></div>
15
+
14
16
  <div class="field-info-container">
15
17
  <div class="field-name">Unit</div>
16
18
  <div class="field-value">{{n-a @resource.unit}}</div>
17
19
  </div>
18
20
 
19
21
  <div class="field-info-container">
20
- <div class="field-name">Status</div>
21
- <div class="field-value">
22
- <Badge @status={{@resource.status}}>{{smart-humanize @resource.status}}</Badge>
23
- </div>
22
+ <div class="field-name">Internal ID</div>
23
+ <div class="field-value">{{n-a @resource.internal_id}}</div>
24
+ </div>
25
+
26
+ <div class="field-info-container col-span-2">
27
+ <div class="field-name">Serial Number</div>
28
+ <div class="field-value">{{n-a @resource.serial_number}}</div>
24
29
  </div>
25
30
 
31
+ </div>
32
+ </ContentPanel>
33
+
34
+ <ContentPanel @title="Thresholds" @open={{true}} @wrapperClass="bordered-top">
35
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
26
36
  <div class="field-info-container">
27
37
  <div class="field-name">Minimum Threshold</div>
28
38
  <div class="field-value">{{n-a @resource.min_threshold}}</div>
@@ -39,29 +49,58 @@
39
49
  {{#if @resource.threshold_inclusive}}
40
50
  <Badge @status="success">Yes</Badge>
41
51
  {{else}}
42
- <Badge @status="default">No</Badge>
52
+ <Badge @status="warning">No</Badge>
43
53
  {{/if}}
44
54
  </div>
45
55
  </div>
46
56
 
47
57
  <div class="field-info-container">
48
- <div class="field-name">Report Frequency</div>
49
- <div class="field-value">{{n-a @resource.report_frequency_sec}} seconds</div>
58
+ <div class="field-name">Threshold Status</div>
59
+ <div class="field-value">
60
+ {{#if (eq @resource.threshold_status "normal")}}
61
+ <Badge @status="success">Normal</Badge>
62
+ {{else if (eq @resource.threshold_status "out_of_range")}}
63
+ <Badge @status="danger">Out of Range</Badge>
64
+ {{else if (eq @resource.threshold_status "above_maximum")}}
65
+ <Badge @status="warning">Above Maximum</Badge>
66
+ {{else if (eq @resource.threshold_status "below_minimum")}}
67
+ <Badge @status="warning">Below Minimum</Badge>
68
+ {{else}}
69
+ <Badge @status="default">{{smart-humanize @resource.threshold_status}}</Badge>
70
+ {{/if}}
71
+ </div>
50
72
  </div>
73
+ </div>
74
+ </ContentPanel>
51
75
 
76
+ <ContentPanel @title="Readings" @open={{true}} @wrapperClass="bordered-top">
77
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
52
78
  <div class="field-info-container">
53
- <div class="field-name">Device</div>
54
- <div class="field-value">{{n-a @resource.device_name}}</div>
79
+ <div class="field-name">Last Reading At</div>
80
+ <div class="field-value">{{n-a (format-date @resource.last_reading_at)}}</div>
55
81
  </div>
56
82
 
57
83
  <div class="field-info-container">
58
- <div class="field-name">Warranty</div>
59
- <div class="field-value">{{n-a @resource.warranty_name}}</div>
84
+ <div class="field-name">Report Frequency</div>
85
+ <div class="field-value">
86
+ {{#if @resource.report_frequency_sec}}
87
+ {{@resource.report_frequency_sec}}
88
+ seconds
89
+ {{else}}
90
+ {{n-a null}}
91
+ {{/if}}
92
+ </div>
60
93
  </div>
94
+ </div>
95
+ </ContentPanel>
61
96
 
97
+ <ContentPanel @title="Status" @open={{true}} @wrapperClass="bordered-top">
98
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
62
99
  <div class="field-info-container">
63
- <div class="field-name">Attached To</div>
64
- <div class="field-value">{{n-a @resource.attached_to_name}}</div>
100
+ <div class="field-name">Status</div>
101
+ <div class="field-value">
102
+ <Badge @status={{@resource.status}}>{{smart-humanize @resource.status}}</Badge>
103
+ </div>
65
104
  </div>
66
105
 
67
106
  <div class="field-info-container">
@@ -74,35 +113,22 @@
74
113
  {{/if}}
75
114
  </div>
76
115
  </div>
116
+ </div>
117
+ </ContentPanel>
77
118
 
119
+ <ContentPanel @title="Integration & Associations" @open={{true}} @wrapperClass="bordered-top">
120
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
78
121
  <div class="field-info-container">
79
- <div class="field-name">Last Reading</div>
80
- <div class="field-value">{{n-a @resource.last_reading_formatted}}</div>
81
- </div>
82
-
83
- <div class="field-info-container">
84
- <div class="field-name">Last Reading At</div>
85
- <div class="field-value">{{format-date @resource.last_reading_at}}</div>
122
+ <div class="field-name">Device</div>
123
+ <div class="field-value">{{n-a @resource.device.name}}</div>
86
124
  </div>
87
125
 
88
126
  <div class="field-info-container">
89
- <div class="field-name">Threshold Status</div>
90
- <div class="field-value">
91
- {{#if (eq @resource.threshold_status "normal")}}
92
- <Badge @status="success">Normal</Badge>
93
- {{else if (eq @resource.threshold_status "out_of_range")}}
94
- <Badge @status="danger">Out of Range</Badge>
95
- {{else if (eq @resource.threshold_status "above_maximum")}}
96
- <Badge @status="warning">Above Maximum</Badge>
97
- {{else if (eq @resource.threshold_status "below_minimum")}}
98
- <Badge @status="warning">Below Minimum</Badge>
99
- {{else}}
100
- <Badge @status="default">{{smart-humanize @resource.threshold_status}}</Badge>
101
- {{/if}}
102
- </div>
127
+ <div class="field-name">Warranty</div>
128
+ <div class="field-value">{{n-a @resource.warranty.name}}</div>
103
129
  </div>
104
130
  </div>
105
131
  </ContentPanel>
106
132
 
107
133
  <CustomField::Yield @subject={{@resource}} @viewMode={{true}} @wrapperClass="bordered-top" />
108
- </div>
134
+ </div>