@fleetbase/fleetops-engine 0.6.21 → 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/device/card.hbs +1 -0
- package/addon/components/device/card.js +3 -0
- package/addon/components/device/manager.hbs +29 -0
- package/addon/components/device/manager.js +95 -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 +13 -2
- package/addon/components/driver/pill.hbs +17 -0
- package/addon/components/driver/pill.js +3 -0
- package/addon/components/map/drawer/device-event-listing.hbs +6 -0
- package/addon/components/map/drawer/position-listing.hbs +35 -19
- package/addon/components/map/drawer/position-listing.js +230 -64
- 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/pill.hbs +34 -0
- package/addon/components/order/pill.js +3 -0
- package/addon/components/positions-replay.hbs +26 -20
- package/addon/components/positions-replay.js +100 -63
- package/addon/components/telematic/form.hbs +4 -4
- package/addon/components/vehicle/card.hbs +1 -1
- 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/management/vehicles/index/details.js +5 -0
- package/addon/routes/management/drivers/index/details/positions.js +3 -0
- package/addon/routes.js +3 -0
- package/addon/services/position-playback.js +486 -0
- package/addon/services/resource-metadata.js +46 -0
- package/addon/templates/management/drivers/index/details/positions.hbs +2 -0
- package/addon/templates/management/vehicles/index/details/devices.hbs +1 -2
- package/app/components/device/card.js +1 -0
- package/app/components/device/manager.js +1 -0
- package/app/components/device/pill.js +1 -0
- package/app/components/driver/pill.js +1 -0
- package/app/components/modals/attach-device.js +1 -0
- package/app/components/order/pill.js +1 -0
- package/app/components/vehicle/pill.js +1 -0
- package/app/routes/management/drivers/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/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +2 -2
- package/server/src/Http/Controllers/{Internal/v1/TelematicWebhookController.php → TelematicWebhookController.php} +1 -2
- package/server/src/Http/Resources/v1/Position.php +1 -1
- package/server/src/Models/Asset.php +10 -8
- package/server/src/Models/Device.php +11 -6
- package/server/src/Models/DeviceEvent.php +26 -3
- package/server/src/Models/Maintenance.php +15 -12
- package/server/src/Models/Part.php +2 -0
- package/server/src/Models/Position.php +10 -0
- package/server/src/Models/TrackingNumber.php +3 -1
- package/server/src/Models/WorkOrder.php +8 -5
- package/server/src/routes.php +12 -0
|
@@ -12,29 +12,38 @@ import getModelName from '@fleetbase/ember-core/utils/get-model-name';
|
|
|
12
12
|
export default class PositionsReplayComponent extends Component {
|
|
13
13
|
@service store;
|
|
14
14
|
@service fetch;
|
|
15
|
-
@service
|
|
15
|
+
@service positionPlayback;
|
|
16
16
|
@service notifications;
|
|
17
17
|
@service location;
|
|
18
18
|
|
|
19
19
|
/** Component ID */
|
|
20
20
|
id = guidFor(this);
|
|
21
21
|
|
|
22
|
-
/** Tracked properties */
|
|
22
|
+
/** Tracked properties - only what's NOT managed by service */
|
|
23
23
|
@tracked positions = [];
|
|
24
24
|
@tracked selectedOrder = null;
|
|
25
25
|
@tracked dateFilter = [format(startOfWeek(new Date(), { weekStartsOn: 1 }), 'yyyy-MM-dd'), format(endOfWeek(new Date(), { weekStartsOn: 1 }), 'yyyy-MM-dd')];
|
|
26
26
|
@tracked map = null;
|
|
27
|
-
@tracked isReplaying = false;
|
|
28
27
|
@tracked replaySpeed = '1';
|
|
29
|
-
@tracked currentReplayIndex = 0;
|
|
30
28
|
@tracked metrics = null;
|
|
31
29
|
@tracked latitude = this.args.resource.latitude || this.location.getLatitude();
|
|
32
30
|
@tracked longitude = this.args.resource.longitude || this.location.getLongitude();
|
|
33
31
|
@tracked zoom = 14;
|
|
34
32
|
@tracked tileUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
|
|
35
|
-
@tracked channelId = null;
|
|
36
33
|
|
|
37
|
-
/**
|
|
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
|
+
|
|
38
47
|
get replayProgressWidth() {
|
|
39
48
|
return htmlSafe(`width: ${this.replayProgress}%;`);
|
|
40
49
|
}
|
|
@@ -179,6 +188,13 @@ export default class PositionsReplayComponent extends Component {
|
|
|
179
188
|
this.loadPositions.perform();
|
|
180
189
|
}
|
|
181
190
|
|
|
191
|
+
willDestroy() {
|
|
192
|
+
super.willDestroy?.();
|
|
193
|
+
|
|
194
|
+
// Clean up replay tracker on component destroy
|
|
195
|
+
this.positionPlayback.reset();
|
|
196
|
+
}
|
|
197
|
+
|
|
182
198
|
/** Actions */
|
|
183
199
|
@action didLoadMap({ target: map }) {
|
|
184
200
|
this.map = map;
|
|
@@ -210,6 +226,11 @@ export default class PositionsReplayComponent extends Component {
|
|
|
210
226
|
|
|
211
227
|
@action onSpeedChanged(speed) {
|
|
212
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
|
+
}
|
|
213
234
|
}
|
|
214
235
|
|
|
215
236
|
@action startReplay() {
|
|
@@ -217,12 +238,47 @@ export default class PositionsReplayComponent extends Component {
|
|
|
217
238
|
this.notifications.warning('No positions to replay');
|
|
218
239
|
return;
|
|
219
240
|
}
|
|
220
|
-
|
|
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();
|
|
221
264
|
}
|
|
222
265
|
|
|
223
266
|
@action stopReplay() {
|
|
224
|
-
this.
|
|
225
|
-
|
|
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);
|
|
226
282
|
}
|
|
227
283
|
|
|
228
284
|
@action clearFilters() {
|
|
@@ -269,13 +325,19 @@ export default class PositionsReplayComponent extends Component {
|
|
|
269
325
|
if (this.positions?.length) {
|
|
270
326
|
yield this.loadMetrics.perform();
|
|
271
327
|
|
|
272
|
-
const bounds = positions
|
|
328
|
+
const bounds = positions
|
|
329
|
+
.filter(({ latitude, longitude }) => this.#isValidLatLng(latitude, longitude))
|
|
330
|
+
.map((pos) => pos.latLng)
|
|
331
|
+
.filter(Boolean);
|
|
273
332
|
const lastFiveBounds = bounds.slice(-5);
|
|
274
333
|
this.map.flyToBounds(lastFiveBounds, {
|
|
275
334
|
animate: true,
|
|
276
335
|
zoom: 16,
|
|
277
336
|
});
|
|
278
337
|
}
|
|
338
|
+
|
|
339
|
+
// Reset replay state when positions change
|
|
340
|
+
this.stopReplay();
|
|
279
341
|
} catch (error) {
|
|
280
342
|
this.notifications.serverError(error);
|
|
281
343
|
}
|
|
@@ -301,65 +363,36 @@ export default class PositionsReplayComponent extends Component {
|
|
|
301
363
|
}
|
|
302
364
|
}
|
|
303
365
|
|
|
304
|
-
|
|
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() {
|
|
305
373
|
if (!this.args.resource) {
|
|
306
374
|
this.notifications.warning('No resource provided for replay');
|
|
307
375
|
return;
|
|
308
376
|
}
|
|
309
377
|
|
|
310
|
-
|
|
311
|
-
this.
|
|
312
|
-
|
|
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;
|
|
378
|
+
if (this.positions.length === 0) {
|
|
379
|
+
this.notifications.warning('No positions to replay');
|
|
380
|
+
return;
|
|
362
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
|
+
});
|
|
363
396
|
}
|
|
364
397
|
|
|
365
398
|
#setResourceLayer(model, layer) {
|
|
@@ -369,4 +402,8 @@ export default class PositionsReplayComponent extends Component {
|
|
|
369
402
|
set(layer, 'record_id', model.id);
|
|
370
403
|
set(layer, 'record_type', type);
|
|
371
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
|
+
}
|
|
372
409
|
}
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
<InputGroup @value={{@resource.name}} @name="Integration Name" />
|
|
50
50
|
|
|
51
51
|
{{#if this.selectedProvider.supports_webhooks}}
|
|
52
|
-
<InputGroup @name="Webhook URL" @helpText={{concat "Configure the URL in your " this.selectedProvider.label " dashboard to receive real-time updates."}}>
|
|
53
|
-
<ClickToCopy @value={{this.selectedProvider.webhook_url}}>
|
|
54
|
-
<Input @value={{this.selectedProvider.webhook_url}} class="form-input" readonly />
|
|
52
|
+
<InputGroup @name="Webhook URL" @helpText={{concat "Configure the URL in your " this.selectedProvider.label " dashboard to receive real-time updates."}} @wrapperClass="col-span-2">
|
|
53
|
+
<ClickToCopy @value={{this.selectedProvider.webhook_url}} class="w-full">
|
|
54
|
+
<Input @value={{this.selectedProvider.webhook_url}} class="form-input w-full" readonly />
|
|
55
55
|
</ClickToCopy>
|
|
56
|
-
<small class="form-text text-muted">
|
|
56
|
+
<small class="form-text text-muted mt-1">
|
|
57
57
|
Configure this URL in your
|
|
58
58
|
{{this.selectedProvider.label}}
|
|
59
59
|
dashboard to receive real-time updates.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<Layout::Resource::Card ...attributes as |Card|>
|
|
2
|
-
<Card.header class={{@headerClass}}>
|
|
2
|
+
<Card.header class="{{@headerClass}} truncate">
|
|
3
3
|
<div class="font-semibold">{{or @resource.name @resource.yearMakeModel}}</div>
|
|
4
4
|
<div class="text-gray-300 dark:text-gray-500 text-sm">{{or @resource.plate_number @resource.vin @resource.serial_number @resource.call_sign @resource.public_id}}</div>
|
|
5
5
|
{{#if (has-block "header")}}
|
|
@@ -47,4 +47,8 @@
|
|
|
47
47
|
</div>
|
|
48
48
|
</div>
|
|
49
49
|
</ContentPanel>
|
|
50
|
+
|
|
51
|
+
<ContentPanel @title={{t "common.metadata"}} @open={{true}} @actionButtons={{this.metadataButtons}} @wrapperClass="bordered-top" @panelBodyWrapperClass={{unless (is-object-empty @resource.meta) "px-0i" ""}}>
|
|
52
|
+
<MetadataViewer @metadata={{@resource.meta}} />
|
|
53
|
+
</ContentPanel>
|
|
50
54
|
</div>
|
|
@@ -1,3 +1,21 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
|
+
import { inject as service } from '@ember/service';
|
|
2
3
|
|
|
3
|
-
export default class VehicleDetailsComponent extends Component {
|
|
4
|
+
export default class VehicleDetailsComponent extends Component {
|
|
5
|
+
@service resourceMetadata;
|
|
6
|
+
|
|
7
|
+
get metadataButtons() {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
type: 'default',
|
|
11
|
+
text: 'Edit',
|
|
12
|
+
icon: 'pencil',
|
|
13
|
+
iconPrefix: 'fas',
|
|
14
|
+
permission: 'fleet-ops update vehicle',
|
|
15
|
+
onClick: () => {
|
|
16
|
+
this.resourceMetadata.edit(this.args.resource);
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -126,4 +126,8 @@
|
|
|
126
126
|
<ContentPanel @title={{t "avatar-picker.avatar"}} @open={{true}} @wrapperClass="bordered-top">
|
|
127
127
|
<AvatarPicker @model={{@resource}} @defaultAvatar={{config "defaultValues.vehicleAvatar"}} @disabled={{cannot-write @resource}} />
|
|
128
128
|
</ContentPanel>
|
|
129
|
+
|
|
130
|
+
<ContentPanel @title={{t "common.metadata"}} @open={{true}} @wrapperClass="bordered-top">
|
|
131
|
+
<MetadataEditor @value={{@resource.meta}} @onChange={{fn (mut @resource.meta)}} />
|
|
132
|
+
</ContentPanel>
|
|
129
133
|
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{{#let (or @vehicle @resource) as |resource|}}
|
|
2
|
+
<Pill
|
|
3
|
+
@resource={{resource}}
|
|
4
|
+
@imageSrc={{resource.photo_url}}
|
|
5
|
+
@fallbackImageType="vehicleImage"
|
|
6
|
+
@showOnlineIndicator={{true}}
|
|
7
|
+
@onClick={{@onClick}}
|
|
8
|
+
@anchorClass={{@anchorClass}}
|
|
9
|
+
@imageClass={{@imageClass}}
|
|
10
|
+
@imageWrapperClass={{@imageWrapperClass}}
|
|
11
|
+
@contentWrapperClass={{@contentWrapperClass}}
|
|
12
|
+
@titleClass={{@titleClass}}
|
|
13
|
+
@subtitleClass={{@subtitleClass}}
|
|
14
|
+
...attributes
|
|
15
|
+
>
|
|
16
|
+
<:default>
|
|
17
|
+
<div class="text-sm">{{n-a resource.name resource.yearMakeModel}}</div>
|
|
18
|
+
<div class="text-xs text-gray-400 dark:text-gray-500">{{or resource.plate_number resource.vin resource.serial_number resource.call_sign}}</div>
|
|
19
|
+
</:default>
|
|
20
|
+
<:tooltip>
|
|
21
|
+
<div>
|
|
22
|
+
<div class="text-xs font-semibold">{{resource.name resource.yearMakeModel}}</div>
|
|
23
|
+
<div class="text-xs">{{t "resource.driver"}}: {{n-a resource.driver_name}}</div>
|
|
24
|
+
<div class="text-xs">
|
|
25
|
+
<span>{{t "common.status"}}:</span>
|
|
26
|
+
<span class="{{if resource.online 'text-green-500' 'text-red-400'}}">
|
|
27
|
+
{{if resource.online (t 'common.online') (t 'common.offline')}}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="text-xs truncate">Pos: {{point-coordinates resource.location}}</div>
|
|
31
|
+
</div>
|
|
32
|
+
</:tooltip>
|
|
33
|
+
</Pill>
|
|
34
|
+
{{/let}}
|
|
@@ -14,8 +14,13 @@ export default class ManagementVehiclesIndexDetailsController extends Controller
|
|
|
14
14
|
route: 'management.vehicles.index.details.positions',
|
|
15
15
|
label: 'Positions',
|
|
16
16
|
},
|
|
17
|
+
{
|
|
18
|
+
route: 'management.vehicles.index.details.devices',
|
|
19
|
+
label: 'Devices',
|
|
20
|
+
},
|
|
17
21
|
];
|
|
18
22
|
}
|
|
23
|
+
|
|
19
24
|
get actionButtons() {
|
|
20
25
|
return [
|
|
21
26
|
{
|
package/addon/routes.js
CHANGED
|
@@ -62,6 +62,7 @@ export default buildRoutes(function () {
|
|
|
62
62
|
this.route('new');
|
|
63
63
|
this.route('details', { path: '/:public_id' }, function () {
|
|
64
64
|
this.route('index', { path: '/' });
|
|
65
|
+
this.route('positions');
|
|
65
66
|
});
|
|
66
67
|
this.route('edit', { path: '/edit/:public_id' });
|
|
67
68
|
});
|
|
@@ -72,6 +73,8 @@ export default buildRoutes(function () {
|
|
|
72
73
|
this.route('details', { path: '/:public_id' }, function () {
|
|
73
74
|
this.route('index', { path: '/' });
|
|
74
75
|
this.route('positions');
|
|
76
|
+
this.route('devices');
|
|
77
|
+
this.route('equipment');
|
|
75
78
|
});
|
|
76
79
|
this.route('edit', { path: '/edit/:public_id' });
|
|
77
80
|
});
|