@fleetbase/fleetops-engine 0.6.19 → 0.6.21

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 (130) hide show
  1. package/addon/components/custom-entity/form.hbs +14 -14
  2. package/addon/components/device/details.hbs +92 -43
  3. package/addon/components/device/form.hbs +108 -60
  4. package/addon/components/device/form.js +36 -8
  5. package/addon/components/device/panel-header.hbs +32 -0
  6. package/addon/components/device/panel-header.js +3 -0
  7. package/addon/components/driver/form.hbs +1 -1
  8. package/addon/components/driver/form.js +49 -47
  9. package/addon/components/entity/form.hbs +7 -5
  10. package/addon/components/layout/fleet-ops-sidebar.js +12 -12
  11. package/addon/components/map/drawer/device-event-listing.hbs +58 -0
  12. package/addon/components/map/drawer/device-event-listing.js +181 -0
  13. package/addon/components/map/drawer/position-listing.hbs +84 -0
  14. package/addon/components/map/drawer/position-listing.js +289 -0
  15. package/addon/components/map/drawer.js +2 -0
  16. package/addon/components/map/leaflet-live-map.hbs +7 -2
  17. package/addon/components/order/details/payload.hbs +6 -4
  18. package/addon/components/order/details/payload.js +2 -0
  19. package/addon/components/order/kanban.hbs +12 -10
  20. package/addon/components/order/kanban.js +27 -3
  21. package/addon/components/order-config-manager/custom-fields.js +1 -1
  22. package/addon/components/positions-replay.hbs +333 -0
  23. package/addon/components/positions-replay.js +372 -0
  24. package/addon/components/sensor/details.hbs +64 -38
  25. package/addon/components/sensor/form.hbs +112 -63
  26. package/addon/components/sensor/form.js +36 -24
  27. package/addon/components/sensor/panel-header.hbs +32 -0
  28. package/addon/components/sensor/panel-header.js +3 -0
  29. package/addon/components/telematic/details.hbs +40 -16
  30. package/addon/components/telematic/form.hbs +63 -64
  31. package/addon/components/telematic/form.js +73 -4
  32. package/addon/components/vehicle/card.hbs +1 -1
  33. package/addon/controllers/analytics/reports/index/edit.js +1 -1
  34. package/addon/controllers/connectivity/devices/index/details.js +22 -1
  35. package/addon/controllers/connectivity/devices/index/edit.js +66 -1
  36. package/addon/controllers/connectivity/devices/index.js +51 -9
  37. package/addon/controllers/connectivity/events/index.js +65 -16
  38. package/addon/controllers/connectivity/sensors/index/details.js +22 -1
  39. package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
  40. package/addon/controllers/connectivity/sensors/index.js +66 -6
  41. package/addon/controllers/connectivity/telematics/index/details.js +22 -1
  42. package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
  43. package/addon/controllers/connectivity/telematics/index.js +20 -11
  44. package/addon/controllers/management/fleets/index/details.js +26 -21
  45. package/addon/controllers/management/fleets/index/edit.js +9 -6
  46. package/addon/controllers/management/vehicles/index/details.js +21 -13
  47. package/addon/controllers/operations/orders/index/new.js +4 -2
  48. package/addon/controllers/operations/orders/index.js +50 -45
  49. package/addon/controllers/settings/custom-fields.js +6 -0
  50. package/addon/helpers/get-fleet-ops-option-label.js +11 -0
  51. package/addon/routes/connectivity/devices/index/details.js +27 -1
  52. package/addon/routes/connectivity/devices/index/edit.js +27 -1
  53. package/addon/routes/connectivity/sensors/index/details.js +27 -1
  54. package/addon/routes/connectivity/sensors/index/edit.js +27 -1
  55. package/addon/routes/connectivity/telematics/index/details.js +27 -1
  56. package/addon/routes/connectivity/telematics/index/edit.js +27 -1
  57. package/addon/routes/management/vehicles/index/details/positions.js +3 -0
  58. package/addon/routes/operations/orders/index.js +0 -3
  59. package/addon/routes.js +1 -0
  60. package/addon/services/movement-tracker.js +81 -30
  61. package/addon/services/order-creation.js +4 -8
  62. package/addon/services/order-validation.js +3 -3
  63. package/addon/styles/fleetops-engine.css +192 -0
  64. package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
  65. package/addon/templates/connectivity/devices/index/details.hbs +15 -2
  66. package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
  67. package/addon/templates/connectivity/events/index.hbs +1 -1
  68. package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
  69. package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
  70. package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
  71. package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
  72. package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
  73. package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
  74. package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
  75. package/addon/templates/operations/orders/index.hbs +26 -2
  76. package/addon/utils/fleet-ops-options.js +95 -0
  77. package/addon/utils/setup-customer-portal.js +7 -0
  78. package/app/components/device/panel-header.js +1 -0
  79. package/app/components/map/drawer/device-event-listing.js +1 -0
  80. package/app/components/map/drawer/position-listing.js +1 -0
  81. package/app/components/positions-replay.js +1 -0
  82. package/app/components/sensor/panel-header.js +1 -0
  83. package/app/helpers/get-fleet-ops-option-label.js +1 -0
  84. package/app/routes/management/vehicles/index/details/positions.js +1 -0
  85. package/app/templates/management/vehicles/index/details/positions.js +1 -0
  86. package/composer.json +1 -1
  87. package/extension.json +1 -1
  88. package/package.json +4 -4
  89. package/server/config/telematics.php +111 -0
  90. package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
  91. package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
  92. package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
  93. package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
  94. package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
  95. package/server/src/Contracts/TelematicProviderInterface.php +119 -0
  96. package/server/src/Exceptions/TelematicProviderException.php +14 -0
  97. package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
  98. package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
  99. package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
  100. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
  101. package/server/src/Http/Controllers/Internal/v1/OrderController.php +50 -68
  102. package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
  103. package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
  104. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
  105. package/server/src/Http/Controllers/Internal/v1/TelematicWebhookController.php +170 -0
  106. package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
  107. package/server/src/Http/Filter/PositionFilter.php +35 -0
  108. package/server/src/Http/Resources/v1/Position.php +44 -0
  109. package/server/src/Jobs/ReplayPositions.php +64 -0
  110. package/server/src/Jobs/SendPositionReplay.php +65 -0
  111. package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
  112. package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
  113. package/server/src/Models/Device.php +72 -10
  114. package/server/src/Models/DeviceEvent.php +7 -0
  115. package/server/src/Models/Driver.php +28 -1
  116. package/server/src/Models/Payload.php +11 -3
  117. package/server/src/Models/Place.php +9 -2
  118. package/server/src/Models/Position.php +17 -17
  119. package/server/src/Models/Sensor.php +78 -13
  120. package/server/src/Models/Telematic.php +116 -6
  121. package/server/src/Models/Vehicle.php +104 -1
  122. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  123. package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
  124. package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
  125. package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
  126. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
  127. package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
  128. package/server/src/Support/Telematics/TelematicService.php +223 -0
  129. package/server/src/Support/Utils.php +1 -1
  130. package/server/src/routes.php +12 -1
@@ -0,0 +1,372 @@
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 movementTracker;
16
+ @service notifications;
17
+ @service location;
18
+
19
+ /** Component ID */
20
+ id = guidFor(this);
21
+
22
+ /** Tracked properties */
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 isReplaying = false;
28
+ @tracked replaySpeed = '1';
29
+ @tracked currentReplayIndex = 0;
30
+ @tracked metrics = null;
31
+ @tracked latitude = this.args.resource.latitude || this.location.getLatitude();
32
+ @tracked longitude = this.args.resource.longitude || this.location.getLongitude();
33
+ @tracked zoom = 14;
34
+ @tracked tileUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
35
+ @tracked channelId = null;
36
+
37
+ /** computed */
38
+ get replayProgressWidth() {
39
+ return htmlSafe(`width: ${this.replayProgress}%;`);
40
+ }
41
+
42
+ get orderFilters() {
43
+ const params = {};
44
+
45
+ if (this.resourceType === 'vehicle') {
46
+ params.vehicle_assigned_uuid = this.resource?.id;
47
+ }
48
+
49
+ if (this.resourceType === 'driver') {
50
+ params.driver_assigned_uuid = this.resource?.id;
51
+ }
52
+
53
+ return params;
54
+ }
55
+
56
+ get resource() {
57
+ return this.args.resource;
58
+ }
59
+
60
+ get resourceName() {
61
+ if (!this.resource) {
62
+ return 'Unknown';
63
+ }
64
+ return this.resource.name || this.resource.display_name || this.resource.displayName || this.resource.public_id || 'Resource';
65
+ }
66
+
67
+ get resourceType() {
68
+ if (!this.resource) {
69
+ return 'resource';
70
+ }
71
+ return getModelName(this.resource) || 'resource';
72
+ }
73
+
74
+ get hasPositions() {
75
+ return this.positions.length > 0;
76
+ }
77
+
78
+ get totalPositions() {
79
+ return this.positions.length;
80
+ }
81
+
82
+ get replayProgress() {
83
+ if (this.totalPositions === 0) {
84
+ return 0;
85
+ }
86
+ return Math.round((this.currentReplayIndex / this.totalPositions) * 100);
87
+ }
88
+
89
+ get firstPosition() {
90
+ return this.positions.length > 0 ? this.positions[0] : null;
91
+ }
92
+
93
+ get lastPosition() {
94
+ return this.positions.length > 0 ? this.positions[this.positions.length - 1] : null;
95
+ }
96
+
97
+ get totalDistance() {
98
+ return this.metrics?.total_distance ?? 0;
99
+ }
100
+
101
+ get totalDuration() {
102
+ return this.metrics?.total_duration ?? 0;
103
+ }
104
+
105
+ get maxSpeed() {
106
+ return this.metrics?.max_speed ?? 0;
107
+ }
108
+
109
+ get avgSpeed() {
110
+ return this.metrics?.avg_speed ?? 0;
111
+ }
112
+
113
+ get speedingCount() {
114
+ return this.metrics?.speeding_count ?? 0;
115
+ }
116
+
117
+ get dwellCount() {
118
+ return this.metrics?.dwell_count ?? 0;
119
+ }
120
+
121
+ get accelerationCount() {
122
+ return this.metrics?.acceleration_count ?? 0;
123
+ }
124
+
125
+ get formattedDuration() {
126
+ const seconds = this.totalDuration;
127
+ const days = Math.floor(seconds / 86400);
128
+ const hours = Math.floor((seconds % 86400) / 3600);
129
+ const minutes = Math.floor((seconds % 3600) / 60);
130
+ const secs = seconds % 60;
131
+
132
+ if (days > 0) {
133
+ return `${days}d ${hours}h ${minutes}m ${secs}s`;
134
+ } else if (hours > 0) {
135
+ return `${hours}h ${minutes}m ${secs}s`;
136
+ } else if (minutes > 0) {
137
+ return `${minutes}m ${secs}s`;
138
+ } else {
139
+ return `${secs}s`;
140
+ }
141
+ }
142
+
143
+ /** Constants */
144
+ speedOptions = [
145
+ { label: '0.5x', value: '0.5' },
146
+ { label: '1x', value: '1' },
147
+ { label: '2x', value: '2' },
148
+ { label: '5x', value: '5' },
149
+ { label: '10x', value: '10' },
150
+ { label: '20x', value: '20' },
151
+ { label: '30x', value: '30' },
152
+ { label: '40x', value: '40' },
153
+ { label: '50x', value: '50' },
154
+ { label: '100x', value: '100' },
155
+ { label: '80x', value: '80' },
156
+ { label: '120x', value: '120' },
157
+ { label: '160x', value: '160' },
158
+ { label: '180x', value: '180' },
159
+ { label: '200x', value: '200' },
160
+ { label: '250x', value: '250' },
161
+ { label: '280x', value: '280' },
162
+ { label: '300x', value: '300' },
163
+ { label: '350x', value: '350' },
164
+ { label: '400x', value: '400' },
165
+ { label: '500x', value: '500' },
166
+ { label: '600x', value: '600' },
167
+ { label: '1000x', value: '1000' },
168
+ ];
169
+
170
+ /** Lifecycle */
171
+ constructor() {
172
+ super(...arguments);
173
+
174
+ // Validate resource argument
175
+ if (!this.args.resource) {
176
+ console.warn('PositionsReplay: @resource argument is required');
177
+ }
178
+
179
+ this.loadPositions.perform();
180
+ }
181
+
182
+ /** Actions */
183
+ @action didLoadMap({ target: map }) {
184
+ this.map = map;
185
+ requestAnimationFrame(() => map.invalidateSize());
186
+
187
+ const hasValidCoordinates = this.args.resource?.hasValidCoordinates || (Number.isFinite(this.args.resource?.latitude) && Number.isFinite(this.args.resource?.longitude));
188
+ if (hasValidCoordinates) {
189
+ const coordinates = [this.args.resource.latitude, this.args.resource.longitude];
190
+
191
+ // Use flyTo with a zoom level of 16 for a smooth animation
192
+ this.map.flyTo(coordinates, 16, {
193
+ animate: true,
194
+ duration: 0.8,
195
+ });
196
+ }
197
+ }
198
+
199
+ @action onOrderSelected(order) {
200
+ this.selectedOrder = order;
201
+ this.loadPositions.perform();
202
+ }
203
+
204
+ @action onDateRangeChanged({ formattedDate }) {
205
+ if (isArray(formattedDate) && formattedDate.length === 2) {
206
+ this.dateFilter = formattedDate;
207
+ this.loadPositions.perform();
208
+ }
209
+ }
210
+
211
+ @action onSpeedChanged(speed) {
212
+ this.replaySpeed = speed;
213
+ }
214
+
215
+ @action startReplay() {
216
+ if (this.positions.length === 0) {
217
+ this.notifications.warning('No positions to replay');
218
+ return;
219
+ }
220
+ this.replayPositions.perform();
221
+ }
222
+
223
+ @action stopReplay() {
224
+ this.isReplaying = false;
225
+ this.currentReplayIndex = 0;
226
+ }
227
+
228
+ @action clearFilters() {
229
+ this.selectedOrder = null;
230
+ this.dateFilter = null;
231
+ this.loadPositions.perform();
232
+ }
233
+
234
+ @action onPositionClicked(position) {
235
+ if (this.map && position.latitude && position.longitude) {
236
+ this.map.setView([position.latitude, position.longitude], this.zoom);
237
+ }
238
+ }
239
+
240
+ @action onTrackingMarkerAdded(resource, { target: layer }) {
241
+ this.#setResourceLayer(resource, layer);
242
+ }
243
+
244
+ /** Tasks */
245
+ @task *loadPositions() {
246
+ if (!this.args.resource) {
247
+ this.notifications.warning('No resource provided for position query');
248
+ return;
249
+ }
250
+
251
+ try {
252
+ const params = {
253
+ limit: 900,
254
+ sort: 'created_at',
255
+ subject_uuid: this.args.resource.id,
256
+ };
257
+
258
+ if (this.selectedOrder) {
259
+ params.order_uuid = this.selectedOrder.id;
260
+ }
261
+
262
+ if (isArray(this.dateFilter) && this.dateFilter.length === 2) {
263
+ params.created_at = this.dateFilter.join(',');
264
+ }
265
+
266
+ const positions = yield this.store.query('position', params);
267
+ this.positions = isArray(positions) ? positions : [];
268
+
269
+ if (this.positions?.length) {
270
+ yield this.loadMetrics.perform();
271
+
272
+ const bounds = positions.map((pos) => pos.latLng).filter(Boolean);
273
+ const lastFiveBounds = bounds.slice(-5);
274
+ this.map.flyToBounds(lastFiveBounds, {
275
+ animate: true,
276
+ zoom: 16,
277
+ });
278
+ }
279
+ } catch (error) {
280
+ this.notifications.serverError(error);
281
+ }
282
+ }
283
+
284
+ @task *loadMetrics() {
285
+ try {
286
+ const positionIds = this.positions.map((p) => p.id);
287
+
288
+ if (positionIds.length === 0) {
289
+ return;
290
+ }
291
+
292
+ const response = yield this.fetch.post('positions/metrics', {
293
+ position_ids: positionIds,
294
+ });
295
+
296
+ if (response && response.metrics) {
297
+ this.metrics = response.metrics;
298
+ }
299
+ } catch (error) {
300
+ this.notifications.serverError(error);
301
+ }
302
+ }
303
+
304
+ @task *replayPositions() {
305
+ if (!this.args.resource) {
306
+ this.notifications.warning('No resource provided for replay');
307
+ return;
308
+ }
309
+
310
+ try {
311
+ this.isReplaying = true;
312
+ this.currentReplayIndex = 0;
313
+
314
+ const positionIds = this.positions.map((p) => p.id);
315
+
316
+ if (positionIds.length === 0) {
317
+ this.notifications.warning('No positions to replay');
318
+ this.isReplaying = false;
319
+ return;
320
+ }
321
+
322
+ // Generate unique channel ID for this replay session
323
+ this.channelId = `position.replay.${this.id}.${Date.now()}`;
324
+
325
+ // Start tracking on custom channel
326
+ yield this.movementTracker.track(this.resource, {
327
+ channelId: this.channelId,
328
+ callback: (output) => {
329
+ const {
330
+ data: { additionalData },
331
+ } = output;
332
+
333
+ const leafletLayer = this.resource.leafletLayer;
334
+ if (leafletLayer) {
335
+ const latlng = leafletLayer._slideToLatLng ?? leafletLayer.getLatLng();
336
+ this.map.panTo(latlng, { animate: true });
337
+ }
338
+
339
+ if (additionalData && Number.isFinite(additionalData.index)) {
340
+ this.currentReplayIndex = additionalData.index + 1;
341
+ if (this.currentReplayIndex === this.totalPositions) {
342
+ this.isReplaying = false;
343
+ }
344
+ }
345
+ },
346
+ });
347
+
348
+ // Trigger backend replay
349
+ const response = yield this.fetch.post('positions/replay', {
350
+ position_ids: positionIds,
351
+ channel_id: this.channelId,
352
+ speed: parseFloat(this.replaySpeed),
353
+ subject_uuid: this.args.resource.id,
354
+ });
355
+
356
+ if (response && response.status === 'ok') {
357
+ this.notifications.success('Replay started successfully');
358
+ }
359
+ } catch (error) {
360
+ this.notifications.serverError(error);
361
+ this.isReplaying = false;
362
+ }
363
+ }
364
+
365
+ #setResourceLayer(model, layer) {
366
+ const type = getModelName(model);
367
+
368
+ set(model, 'leafletLayer', layer);
369
+ set(layer, 'record_id', model.id);
370
+ set(layer, 'record_type', type);
371
+ }
372
+ }
@@ -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>