@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.
- package/addon/components/custom-entity/form.hbs +14 -14
- package/addon/components/device/card.hbs +1 -0
- package/addon/components/device/card.js +3 -0
- 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/manager.hbs +29 -0
- package/addon/components/device/manager.js +95 -0
- package/addon/components/device/panel-header.hbs +32 -0
- package/addon/components/device/panel-header.js +3 -0
- package/addon/components/device/pill.hbs +16 -0
- package/addon/components/device/pill.js +3 -0
- package/addon/components/driver/details.hbs +4 -0
- package/addon/components/driver/details.js +19 -1
- package/addon/components/driver/form.hbs +14 -3
- package/addon/components/driver/form.js +49 -47
- package/addon/components/driver/pill.hbs +17 -0
- package/addon/components/driver/pill.js +3 -0
- 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 +64 -0
- package/addon/components/map/drawer/device-event-listing.js +181 -0
- package/addon/components/map/drawer/position-listing.hbs +100 -0
- package/addon/components/map/drawer/position-listing.js +455 -0
- package/addon/components/map/drawer.js +2 -0
- package/addon/components/map/leaflet-live-map.hbs +7 -2
- package/addon/components/modals/attach-device.hbs +18 -0
- package/addon/components/modals/attach-device.js +3 -0
- package/addon/components/order/details/detail.hbs +2 -54
- package/addon/components/order/details/detail.js +1 -0
- package/addon/components/order/details/payload.hbs +6 -4
- package/addon/components/order/details/payload.js +2 -0
- package/addon/components/order/pill.hbs +34 -0
- package/addon/components/order/pill.js +3 -0
- package/addon/components/order-config-manager/custom-fields.js +1 -1
- package/addon/components/positions-replay.hbs +339 -0
- package/addon/components/positions-replay.js +409 -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 +2 -2
- package/addon/components/vehicle/details.hbs +4 -0
- package/addon/components/vehicle/details.js +19 -1
- package/addon/components/vehicle/form.hbs +4 -0
- package/addon/components/vehicle/pill.hbs +34 -0
- package/addon/components/vehicle/pill.js +3 -0
- 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 +26 -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/drivers/index/details/positions.js +3 -0
- package/addon/routes/management/vehicles/index/details/positions.js +3 -0
- package/addon/routes.js +4 -0
- package/addon/services/movement-tracker.js +81 -30
- package/addon/services/position-playback.js +486 -0
- package/addon/services/resource-metadata.js +46 -0
- 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/drivers/index/details/positions.hbs +2 -0
- package/addon/templates/management/vehicles/index/details/devices.hbs +1 -2
- package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
- package/addon/utils/fleet-ops-options.js +95 -0
- package/app/components/device/card.js +1 -0
- package/app/components/device/manager.js +1 -0
- package/app/components/device/panel-header.js +1 -0
- package/app/components/device/pill.js +1 -0
- package/app/components/driver/pill.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/modals/attach-device.js +1 -0
- package/app/components/order/pill.js +1 -0
- package/app/components/positions-replay.js +1 -0
- package/app/components/sensor/panel-header.js +1 -0
- package/app/components/vehicle/pill.js +1 -0
- package/app/helpers/get-fleet-ops-option-label.js +1 -0
- package/app/routes/management/drivers/index/details/positions.js +1 -0
- package/app/routes/management/vehicles/index/details/positions.js +1 -0
- package/app/services/position-playback.js +1 -0
- package/app/services/resource-metadata.js +1 -0
- package/app/templates/management/drivers/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/TelematicWebhookController.php +169 -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/Asset.php +10 -8
- package/server/src/Models/Device.php +79 -12
- package/server/src/Models/DeviceEvent.php +33 -3
- package/server/src/Models/Driver.php +28 -1
- package/server/src/Models/Maintenance.php +15 -12
- package/server/src/Models/Part.php +2 -0
- package/server/src/Models/Payload.php +0 -1
- package/server/src/Models/Place.php +4 -1
- package/server/src/Models/Position.php +27 -17
- package/server/src/Models/Sensor.php +78 -13
- package/server/src/Models/Telematic.php +116 -6
- package/server/src/Models/TrackingNumber.php +3 -1
- package/server/src/Models/Vehicle.php +8 -11
- package/server/src/Models/WorkOrder.php +8 -5
- 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 +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="
|
|
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>
|