@fleetbase/fleetops-engine 0.6.20 → 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.
- package/addon/components/custom-entity/form.hbs +14 -14
- package/addon/components/device/details.hbs +92 -43
- package/addon/components/device/form.hbs +108 -60
- package/addon/components/device/form.js +36 -8
- package/addon/components/device/panel-header.hbs +32 -0
- package/addon/components/device/panel-header.js +3 -0
- package/addon/components/driver/form.hbs +1 -1
- package/addon/components/driver/form.js +49 -47
- package/addon/components/entity/form.hbs +7 -5
- package/addon/components/layout/fleet-ops-sidebar.js +12 -12
- package/addon/components/map/drawer/device-event-listing.hbs +58 -0
- package/addon/components/map/drawer/device-event-listing.js +181 -0
- package/addon/components/map/drawer/position-listing.hbs +84 -0
- package/addon/components/map/drawer/position-listing.js +289 -0
- package/addon/components/map/drawer.js +2 -0
- package/addon/components/map/leaflet-live-map.hbs +7 -2
- package/addon/components/order/details/payload.hbs +6 -4
- package/addon/components/order/details/payload.js +2 -0
- package/addon/components/order-config-manager/custom-fields.js +1 -1
- package/addon/components/positions-replay.hbs +333 -0
- package/addon/components/positions-replay.js +372 -0
- package/addon/components/sensor/details.hbs +64 -38
- package/addon/components/sensor/form.hbs +112 -63
- package/addon/components/sensor/form.js +36 -24
- package/addon/components/sensor/panel-header.hbs +32 -0
- package/addon/components/sensor/panel-header.js +3 -0
- package/addon/components/telematic/details.hbs +40 -16
- package/addon/components/telematic/form.hbs +63 -64
- package/addon/components/telematic/form.js +73 -4
- package/addon/components/vehicle/card.hbs +1 -1
- package/addon/controllers/analytics/reports/index/edit.js +1 -1
- package/addon/controllers/connectivity/devices/index/details.js +22 -1
- package/addon/controllers/connectivity/devices/index/edit.js +66 -1
- package/addon/controllers/connectivity/devices/index.js +51 -9
- package/addon/controllers/connectivity/events/index.js +65 -16
- package/addon/controllers/connectivity/sensors/index/details.js +22 -1
- package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
- package/addon/controllers/connectivity/sensors/index.js +66 -6
- package/addon/controllers/connectivity/telematics/index/details.js +22 -1
- package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
- package/addon/controllers/connectivity/telematics/index.js +20 -11
- package/addon/controllers/management/fleets/index/details.js +26 -21
- package/addon/controllers/management/fleets/index/edit.js +9 -6
- package/addon/controllers/management/vehicles/index/details.js +21 -13
- package/addon/controllers/settings/custom-fields.js +6 -0
- package/addon/helpers/get-fleet-ops-option-label.js +11 -0
- package/addon/routes/connectivity/devices/index/details.js +27 -1
- package/addon/routes/connectivity/devices/index/edit.js +27 -1
- package/addon/routes/connectivity/sensors/index/details.js +27 -1
- package/addon/routes/connectivity/sensors/index/edit.js +27 -1
- package/addon/routes/connectivity/telematics/index/details.js +27 -1
- package/addon/routes/connectivity/telematics/index/edit.js +27 -1
- package/addon/routes/management/vehicles/index/details/positions.js +3 -0
- package/addon/routes.js +1 -0
- package/addon/services/movement-tracker.js +81 -30
- package/addon/styles/fleetops-engine.css +157 -0
- package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/devices/index/details.hbs +15 -2
- package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
- package/addon/templates/connectivity/events/index.hbs +1 -1
- package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
- package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
- package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
- package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
- package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
- package/addon/utils/fleet-ops-options.js +95 -0
- package/app/components/device/panel-header.js +1 -0
- package/app/components/map/drawer/device-event-listing.js +1 -0
- package/app/components/map/drawer/position-listing.js +1 -0
- package/app/components/positions-replay.js +1 -0
- package/app/components/sensor/panel-header.js +1 -0
- package/app/helpers/get-fleet-ops-option-label.js +1 -0
- package/app/routes/management/vehicles/index/details/positions.js +1 -0
- package/app/templates/management/vehicles/index/details/positions.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +4 -4
- package/server/config/telematics.php +111 -0
- package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
- package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
- package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
- package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
- package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
- package/server/src/Contracts/TelematicProviderInterface.php +119 -0
- package/server/src/Exceptions/TelematicProviderException.php +14 -0
- package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
- package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
- package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicWebhookController.php +170 -0
- package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
- package/server/src/Http/Filter/PositionFilter.php +35 -0
- package/server/src/Http/Resources/v1/Position.php +44 -0
- package/server/src/Jobs/ReplayPositions.php +64 -0
- package/server/src/Jobs/SendPositionReplay.php +65 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
- package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
- package/server/src/Models/Device.php +72 -10
- package/server/src/Models/DeviceEvent.php +7 -0
- package/server/src/Models/Driver.php +28 -1
- package/server/src/Models/Payload.php +0 -1
- package/server/src/Models/Place.php +4 -1
- package/server/src/Models/Position.php +17 -17
- package/server/src/Models/Sensor.php +78 -13
- package/server/src/Models/Telematic.php +116 -6
- package/server/src/Models/Vehicle.php +8 -11
- package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
- package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
- package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
- package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
- package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
- package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
- package/server/src/Support/Telematics/TelematicService.php +223 -0
- package/server/src/Support/Utils.php +1 -1
- 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="
|
|
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">{{
|
|
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">
|
|
21
|
-
<div class="field-value">
|
|
22
|
-
|
|
23
|
-
|
|
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="
|
|
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">
|
|
49
|
-
<div class="field-value">
|
|
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">
|
|
54
|
-
<div class="field-value">{{n-a @resource.
|
|
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">
|
|
59
|
-
<div class="field-value">
|
|
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">
|
|
64
|
-
<div class="field-value">
|
|
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">
|
|
80
|
-
<div class="field-value">{{n-a @resource.
|
|
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">
|
|
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>
|