@fleetbase/fleetops-engine 0.6.21 → 0.6.23
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/Console/Commands/DispatchAdhocOrders.php +47 -75
- package/server/src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php +1 -1
- package/server/src/Console/Commands/TrackOrderDistanceAndTime.php +109 -65
- 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/Order.php +20 -8
- package/server/src/Models/Part.php +2 -0
- package/server/src/Models/Place.php +4 -5
- 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/Providers/FleetOpsServiceProvider.php +1 -1
- package/server/src/routes.php +12 -0
|
@@ -8,23 +8,37 @@ import { htmlSafe } from '@ember/template';
|
|
|
8
8
|
import { startOfWeek, endOfWeek, format } from 'date-fns';
|
|
9
9
|
import getModelName from '@fleetbase/ember-core/utils/get-model-name';
|
|
10
10
|
|
|
11
|
+
const L = window.leaflet || window.L;
|
|
12
|
+
|
|
11
13
|
export default class MapDrawerPositionListingComponent extends Component {
|
|
12
14
|
@service leafletMapManager;
|
|
13
15
|
@service store;
|
|
14
16
|
@service fetch;
|
|
15
|
-
@service
|
|
17
|
+
@service positionPlayback;
|
|
16
18
|
@service hostRouter;
|
|
17
19
|
@service notifications;
|
|
18
|
-
|
|
19
20
|
@service intl;
|
|
21
|
+
|
|
22
|
+
/** Tracked properties - only what's NOT managed by service */
|
|
20
23
|
@tracked positions = [];
|
|
21
24
|
@tracked resource = null;
|
|
22
25
|
@tracked selectedOrder = null;
|
|
23
26
|
@tracked dateFilter = [format(startOfWeek(new Date(), { weekStartsOn: 1 }), 'yyyy-MM-dd'), format(endOfWeek(new Date(), { weekStartsOn: 1 }), 'yyyy-MM-dd')];
|
|
24
|
-
@tracked isReplaying = false;
|
|
25
27
|
@tracked replaySpeed = '1';
|
|
26
|
-
@tracked
|
|
27
|
-
|
|
28
|
+
@tracked positionsLayer = null;
|
|
29
|
+
|
|
30
|
+
/** Computed properties - read state from service */
|
|
31
|
+
get isReplaying() {
|
|
32
|
+
return this.positionPlayback.isPlaying;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get isPaused() {
|
|
36
|
+
return this.positionPlayback.isPaused;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get currentReplayIndex() {
|
|
40
|
+
return this.positionPlayback.currentIndex;
|
|
41
|
+
}
|
|
28
42
|
|
|
29
43
|
get trackables() {
|
|
30
44
|
const vehicles = this.leafletMapManager._livemap?.vehicles ?? [];
|
|
@@ -107,19 +121,27 @@ export default class MapDrawerPositionListingComponent extends Component {
|
|
|
107
121
|
{
|
|
108
122
|
label: '#',
|
|
109
123
|
valuePath: 'index',
|
|
110
|
-
width: '
|
|
124
|
+
width: '80px',
|
|
125
|
+
cellComponent: 'table/cell/anchor',
|
|
126
|
+
onClick: this.onPositionClicked,
|
|
111
127
|
},
|
|
112
128
|
{
|
|
113
129
|
label: 'Timestamp',
|
|
114
130
|
valuePath: 'timestamp',
|
|
131
|
+
cellComponent: 'table/cell/anchor',
|
|
132
|
+
onClick: this.onPositionClicked,
|
|
115
133
|
},
|
|
116
134
|
{
|
|
117
135
|
label: 'Latitude',
|
|
118
136
|
valuePath: 'latitude',
|
|
137
|
+
cellComponent: 'table/cell/anchor',
|
|
138
|
+
onClick: this.onPositionClicked,
|
|
119
139
|
},
|
|
120
140
|
{
|
|
121
141
|
label: 'Longitude',
|
|
122
142
|
valuePath: 'longitude',
|
|
143
|
+
cellComponent: 'table/cell/anchor',
|
|
144
|
+
onClick: this.onPositionClicked,
|
|
123
145
|
},
|
|
124
146
|
{
|
|
125
147
|
label: 'Speed (km/h)',
|
|
@@ -141,8 +163,20 @@ export default class MapDrawerPositionListingComponent extends Component {
|
|
|
141
163
|
this.loadPositions.perform();
|
|
142
164
|
}
|
|
143
165
|
|
|
166
|
+
willDestroy() {
|
|
167
|
+
super.willDestroy?.();
|
|
168
|
+
|
|
169
|
+
// Clean up replay tracker
|
|
170
|
+
this.positionPlayback.reset();
|
|
171
|
+
|
|
172
|
+
// Remove our layer group from the map
|
|
173
|
+
this.#clearPositionsLayer(true);
|
|
174
|
+
this.positionsLayer = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
144
177
|
@action onResourceSelected(resource) {
|
|
145
178
|
this.resource = resource;
|
|
179
|
+
this.focusResource(resource);
|
|
146
180
|
this.loadPositions.perform();
|
|
147
181
|
}
|
|
148
182
|
|
|
@@ -160,6 +194,11 @@ export default class MapDrawerPositionListingComponent extends Component {
|
|
|
160
194
|
|
|
161
195
|
@action onSpeedChanged(speed) {
|
|
162
196
|
this.replaySpeed = speed;
|
|
197
|
+
|
|
198
|
+
// Update replay speed in real-time if currently playing
|
|
199
|
+
if (this.isReplaying) {
|
|
200
|
+
this.positionPlayback.setSpeed(parseFloat(speed));
|
|
201
|
+
}
|
|
163
202
|
}
|
|
164
203
|
|
|
165
204
|
@action startReplay() {
|
|
@@ -167,12 +206,47 @@ export default class MapDrawerPositionListingComponent extends Component {
|
|
|
167
206
|
this.notifications.warning('No positions to replay');
|
|
168
207
|
return;
|
|
169
208
|
}
|
|
170
|
-
|
|
209
|
+
|
|
210
|
+
if (this.isReplaying && !this.isPaused) {
|
|
211
|
+
this.notifications.info('Replay is already running');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// If paused, resume
|
|
216
|
+
if (this.isPaused) {
|
|
217
|
+
this.positionPlayback.play();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Start new replay
|
|
222
|
+
this.#initializeReplay();
|
|
223
|
+
this.positionPlayback.play();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@action pauseReplay() {
|
|
227
|
+
if (!this.isReplaying) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.positionPlayback.pause();
|
|
171
232
|
}
|
|
172
233
|
|
|
173
234
|
@action stopReplay() {
|
|
174
|
-
this.
|
|
175
|
-
|
|
235
|
+
this.positionPlayback.stop();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@action stepForward() {
|
|
239
|
+
if (this.isReplaying) {
|
|
240
|
+
this.pauseReplay();
|
|
241
|
+
}
|
|
242
|
+
this.positionPlayback.stepForward(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@action stepBackward() {
|
|
246
|
+
if (this.isReplaying) {
|
|
247
|
+
this.pauseReplay();
|
|
248
|
+
}
|
|
249
|
+
this.positionPlayback.stepBackward(1);
|
|
176
250
|
}
|
|
177
251
|
|
|
178
252
|
@action clearFilters() {
|
|
@@ -183,7 +257,20 @@ export default class MapDrawerPositionListingComponent extends Component {
|
|
|
183
257
|
|
|
184
258
|
@action onPositionClicked(position) {
|
|
185
259
|
if (this.leafletMapManager.map && position.latitude && position.longitude) {
|
|
186
|
-
this.leafletMapManager.map.setView([position.latitude, position.longitude],
|
|
260
|
+
this.leafletMapManager.map.setView([position.latitude, position.longitude], 16);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@action focusResource(resource) {
|
|
265
|
+
const hasValidCoordinates = resource?.hasValidCoordinates || (Number.isFinite(resource?.latitude) && Number.isFinite(resource?.longitude));
|
|
266
|
+
if (hasValidCoordinates) {
|
|
267
|
+
const coordinates = [resource.latitude, resource.longitude];
|
|
268
|
+
|
|
269
|
+
// Use flyTo with a zoom level of 18 for a smooth animation
|
|
270
|
+
this.leafletMapManager.map.flyTo(coordinates, 18, {
|
|
271
|
+
animate: true,
|
|
272
|
+
duration: 0.8,
|
|
273
|
+
});
|
|
187
274
|
}
|
|
188
275
|
}
|
|
189
276
|
|
|
@@ -213,77 +300,156 @@ export default class MapDrawerPositionListingComponent extends Component {
|
|
|
213
300
|
})
|
|
214
301
|
: [];
|
|
215
302
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
animate: true,
|
|
221
|
-
zoom: 16,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
303
|
+
this.#renderPositionsOnMap({ fitLast: 5 });
|
|
304
|
+
|
|
305
|
+
// Reset replay state when positions change
|
|
306
|
+
this.stopReplay();
|
|
224
307
|
} catch (error) {
|
|
225
308
|
this.notifications.serverError(error);
|
|
226
309
|
}
|
|
227
310
|
}
|
|
228
311
|
|
|
229
|
-
|
|
312
|
+
/**
|
|
313
|
+
* Initialize replay tracker with current positions and settings
|
|
314
|
+
* This replaces the socket-based backend replay approach
|
|
315
|
+
*
|
|
316
|
+
* @private
|
|
317
|
+
*/
|
|
318
|
+
#initializeReplay() {
|
|
230
319
|
if (!this.resource) {
|
|
231
320
|
this.notifications.warning('No resource provided for replay');
|
|
232
321
|
return;
|
|
233
322
|
}
|
|
234
323
|
|
|
235
|
-
|
|
236
|
-
this.
|
|
237
|
-
|
|
324
|
+
if (this.positions.length === 0) {
|
|
325
|
+
this.notifications.warning('No positions to replay');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Initialize replay tracker with positions
|
|
330
|
+
this.positionPlayback.initialize({
|
|
331
|
+
subject: this.resource,
|
|
332
|
+
positions: this.positions,
|
|
333
|
+
speed: parseFloat(this.replaySpeed),
|
|
334
|
+
map: this.leafletMapManager.map,
|
|
335
|
+
callback: (data) => {
|
|
336
|
+
if (data.type === 'complete') {
|
|
337
|
+
// Replay completed
|
|
338
|
+
this.notifications.success('Replay completed');
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}
|
|
238
343
|
|
|
239
|
-
|
|
344
|
+
/** Position Rendering */
|
|
345
|
+
#ensurePositionsLayer() {
|
|
346
|
+
if (!this.leafletMapManager.map) return null;
|
|
240
347
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
348
|
+
if (!this.positionsLayer) {
|
|
349
|
+
this.positionsLayer = L.layerGroup();
|
|
350
|
+
this.positionsLayer.addTo(this.leafletMapManager.map);
|
|
351
|
+
}
|
|
352
|
+
return this.positionsLayer;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#clearPositionsLayer(removeFromMap = false) {
|
|
356
|
+
if (this.positionsLayer) {
|
|
357
|
+
this.positionsLayer.clearLayers();
|
|
358
|
+
if (removeFromMap && this.leafletMapManager.map) {
|
|
359
|
+
this.positionsLayer.removeFrom(this.leafletMapManager.map);
|
|
245
360
|
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
246
363
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
364
|
+
#addPositionMarker(pos, index) {
|
|
365
|
+
const lat = parseFloat(pos.latitude);
|
|
366
|
+
const lng = parseFloat(pos.longitude);
|
|
367
|
+
if (!this.#isValidLatLng(lat, lng)) return;
|
|
368
|
+
|
|
369
|
+
const marker = L.circleMarker([lat, lng], {
|
|
370
|
+
radius: 3,
|
|
371
|
+
color: '#3b82f6',
|
|
372
|
+
fillColor: '#3b82f6',
|
|
373
|
+
fillOpacity: 0.6,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Popup content (mirrors your template)
|
|
377
|
+
const html = htmlSafe(`<div class="text-xs">
|
|
378
|
+
<div><strong>Position ${index + 1}</strong></div>
|
|
379
|
+
<div>Time: ${pos.timestamp ?? ''}</div>
|
|
380
|
+
<div>Speed: ${pos.speedKmh ?? 'N/A'} km/h</div>
|
|
381
|
+
<div>Heading: ${pos.heading ?? 'N/A'}°</div>
|
|
382
|
+
<div>Altitude: ${pos.altitude ?? 'N/A'} m</div>
|
|
383
|
+
</div>`);
|
|
384
|
+
|
|
385
|
+
marker.bindPopup(html);
|
|
386
|
+
|
|
387
|
+
// Click handler -> reuse your action
|
|
388
|
+
marker.on('click', () => this.onPositionClicked(pos));
|
|
389
|
+
|
|
390
|
+
marker.addTo(this.positionsLayer);
|
|
391
|
+
}
|
|
272
392
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
393
|
+
#isValidLatLng(lat, lng) {
|
|
394
|
+
return Number.isFinite(lat) && Number.isFinite(lng) && lat <= 90 && lat >= -90 && lng <= 180 && lng >= -180 && lat !== 0 && lng !== 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#renderPositionsOnMap({ fitLast = 0, minZoom = 15, maxZoom = 18 } = {}) {
|
|
398
|
+
if (!this.leafletMapManager.map || !this.positions?.length) {
|
|
399
|
+
this.#clearPositionsLayer(false);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
this.#ensurePositionsLayer();
|
|
404
|
+
this.positionsLayer.clearLayers();
|
|
280
405
|
|
|
281
|
-
|
|
282
|
-
|
|
406
|
+
const latlngs = [];
|
|
407
|
+
this.positions.forEach((pos, i) => {
|
|
408
|
+
const lat = parseFloat(pos.latitude);
|
|
409
|
+
const lng = parseFloat(pos.longitude);
|
|
410
|
+
if (this.#isValidLatLng(lat, lng)) {
|
|
411
|
+
this.#addPositionMarker(pos, i);
|
|
412
|
+
latlngs.push([lat, lng]);
|
|
283
413
|
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (!latlngs.length) return;
|
|
417
|
+
|
|
418
|
+
// choose subset (e.g., last 5 points) to bias the view local
|
|
419
|
+
const slice = fitLast > 0 ? latlngs.slice(-fitLast) : latlngs;
|
|
420
|
+
|
|
421
|
+
// Clamp zoom to neighborhood-level
|
|
422
|
+
this.#fitNeighborhood(slice, { zoom: 16, minZoom, maxZoom, padding: [24, 24], animate: true });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
#fitNeighborhood(latlngs, { zoom = null, minZoom = 15, maxZoom = 18, padding = [16, 16], animate = true } = {}) {
|
|
426
|
+
if (!this.leafletMapManager.map || !latlngs?.length) return;
|
|
427
|
+
|
|
428
|
+
const map = this.leafletMapManager.map;
|
|
429
|
+
|
|
430
|
+
if (latlngs.length === 1) {
|
|
431
|
+
// Single point → center on it at minZoom
|
|
432
|
+
map.setView(latlngs[0], minZoom, { animate });
|
|
433
|
+
return;
|
|
287
434
|
}
|
|
435
|
+
|
|
436
|
+
const bounds = L.latLngBounds(latlngs);
|
|
437
|
+
// Compute the zoom that would fit the bounds, then clamp it
|
|
438
|
+
// Leaflet getBoundsZoom may be (bounds, inside, padding) depending on version
|
|
439
|
+
let targetZoom = zoom;
|
|
440
|
+
if (!zoom) {
|
|
441
|
+
try {
|
|
442
|
+
// Try newer signature
|
|
443
|
+
targetZoom = map.getBoundsZoom(bounds, true, padding);
|
|
444
|
+
} catch {
|
|
445
|
+
// Fallback without padding param
|
|
446
|
+
targetZoom = map.getBoundsZoom(bounds, true);
|
|
447
|
+
}
|
|
448
|
+
targetZoom = Math.max(minZoom, Math.min(maxZoom, targetZoom));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Center on bounds center with clamped zoom
|
|
452
|
+
const center = bounds.getCenter();
|
|
453
|
+
map.setView(center, targetZoom, { animate });
|
|
288
454
|
}
|
|
289
455
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
|
|
2
|
+
<div class="modal-body-container">
|
|
3
|
+
<InputGroup @name={{t "common.select-field" field=(t "resource.device")}} @value={{@options.driver.vehicle_uuid}}>
|
|
4
|
+
<ModelSelect
|
|
5
|
+
@modelName="device"
|
|
6
|
+
@selectedModel={{@options.selectedDevice}}
|
|
7
|
+
@placeholder={{t "common.select-field" field=(t "resource.device")}}
|
|
8
|
+
@triggerClass="form-select form-input"
|
|
9
|
+
@infiniteScroll={{false}}
|
|
10
|
+
@renderInPlace={{true}}
|
|
11
|
+
@onChange={{fn (mut @options.selectedDevice)}}
|
|
12
|
+
as |model|
|
|
13
|
+
>
|
|
14
|
+
<Device::Pill @device={{model}} />
|
|
15
|
+
</ModelSelect>
|
|
16
|
+
</InputGroup>
|
|
17
|
+
</div>
|
|
18
|
+
</Modal::Default>
|
|
@@ -16,25 +16,7 @@
|
|
|
16
16
|
<span>{{t "order.fields.driver-assigned"}}</span>
|
|
17
17
|
</div>
|
|
18
18
|
<div class="field-value">
|
|
19
|
-
<
|
|
20
|
-
<a href="javascript:;" class="flex flex-row space-x-2" {{on "click" (fn this.focusOrderAssignedDriver @resource.driver_assigned)}}>
|
|
21
|
-
<div class="relative shrink-0">
|
|
22
|
-
<Image
|
|
23
|
-
src={{avatar-url @resource.driver_assigned.photo_url}}
|
|
24
|
-
@fallbackSrc={{config "defaultValues.driverImage"}}
|
|
25
|
-
width="28"
|
|
26
|
-
height="28"
|
|
27
|
-
class="w-7 h-7 rounded-full ring-2 ring-gray-800 dark:ring-gray-700 shadow transition-shadow hover:shadow-md focus:shadow-md"
|
|
28
|
-
alt={{@resource.driver_assigned.name}}
|
|
29
|
-
/>
|
|
30
|
-
<FaIcon @icon="circle" @size="2xs" class="absolute left-0 top-0 h-2 w-2 {{if @resource.driver_assigned.online 'text-green-500' 'text-yellow-200'}}" />
|
|
31
|
-
</div>
|
|
32
|
-
<div>
|
|
33
|
-
<div class="text-sm">{{n-a @resource.driver_assigned.name}}</div>
|
|
34
|
-
<div class="text-xs text-gray-400 dark:text-gray-500">{{n-a @resource.driver_assigned.phone "No Phone"}}</div>
|
|
35
|
-
</div>
|
|
36
|
-
</a>
|
|
37
|
-
</div>
|
|
19
|
+
<Driver::Pill @driver={{@resource.driver_assigned}} @onClick={{this.focusOrderAssignedDriver}} />
|
|
38
20
|
</div>
|
|
39
21
|
</div>
|
|
40
22
|
<div class="field-info-container">
|
|
@@ -42,41 +24,7 @@
|
|
|
42
24
|
<span>{{t "order.fields.vehicle-assigned"}}</span>
|
|
43
25
|
</div>
|
|
44
26
|
<div class="field-value">
|
|
45
|
-
<
|
|
46
|
-
<div class="relative shrink-0">
|
|
47
|
-
<Image
|
|
48
|
-
src={{@resource.vehicle_assigned.photo_url}}
|
|
49
|
-
@fallbackSrc={{config "defaultValues.vehicleImage"}}
|
|
50
|
-
width="28"
|
|
51
|
-
height="28"
|
|
52
|
-
class="w-7 h-7 rounded-full ring-2 ring-gray-800 dark:ring-gray-700 shadow transition-shadow hover:shadow-md focus:shadow-md"
|
|
53
|
-
alt={{n-a @resource.vehicle_assigned.name @resource.vehicle_assigned.yearMakeModel}}
|
|
54
|
-
/>
|
|
55
|
-
<FaIcon @icon="circle" @size="2xs" class="absolute left-0 top-0 h-2 w-2 {{if @resource.vehicle_assigned.online 'text-green-500' 'text-yellow-200'}}" />
|
|
56
|
-
</div>
|
|
57
|
-
<div>
|
|
58
|
-
<div class="text-sm">{{n-a @resource.vehicle_assigned.name @resource.vehicle_assigned.yearMakeModel}}</div>
|
|
59
|
-
<div class="text-xs text-gray-400 dark:text-gray-500">{{or
|
|
60
|
-
@resource.vehicle_assigned.plate_number
|
|
61
|
-
@resource.vehicle_assigned.vin
|
|
62
|
-
@resource.vehicle_assigned.serial_number
|
|
63
|
-
@resource.vehicle_assigned.call_sign
|
|
64
|
-
}}</div>
|
|
65
|
-
</div>
|
|
66
|
-
<Attach::Tooltip @class="clean" @animation="scale" @placement="top">
|
|
67
|
-
<InputInfo>
|
|
68
|
-
<div class="text-xs font-semibold">{{@resource.vehicle_assigned.name @resource.vehicle_assigned.yearMakeModel}}</div>
|
|
69
|
-
<div class="text-xs">Driver: {{n-a @resource.vehicle_assigned.driver_name}}</div>
|
|
70
|
-
<div class="text-xs">
|
|
71
|
-
<span>Status:</span>
|
|
72
|
-
<span class="{{if @resource.vehicle_assigned.online 'text-green-500' 'text-red-400'}}">
|
|
73
|
-
{{if @resource.vehicle_assigned.online "Online" "Offline"}}
|
|
74
|
-
</span>
|
|
75
|
-
</div>
|
|
76
|
-
<div class="text-xs truncate">Pos: {{point-coordinates @resource.vehicle_assigned.location}}</div>
|
|
77
|
-
</InputInfo>
|
|
78
|
-
</Attach::Tooltip>
|
|
79
|
-
</a>
|
|
27
|
+
<Vehicle::Pill @vehicle={{@resource.vehicle_assigned}} />
|
|
80
28
|
</div>
|
|
81
29
|
</div>
|
|
82
30
|
<div class="field-info-container">
|
|
@@ -36,6 +36,7 @@ export default class OrderDetailsDetailComponent extends Component {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
@action focusOrderAssignedDriver(driver) {
|
|
39
|
+
console.log('[focusOrderAssignedDriver]', ...arguments);
|
|
39
40
|
this.driverActions.panel.view(driver);
|
|
40
41
|
this.leafletMapManager.map?.flyTo(driver.coordinates, 18);
|
|
41
42
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{{#let (or @order @resource) as |resource|}}
|
|
2
|
+
<Pill
|
|
3
|
+
@resource={{resource}}
|
|
4
|
+
@onClick={{@onClick}}
|
|
5
|
+
@anchorClass={{@anchorClass}}
|
|
6
|
+
@imageClass={{@imageClass}}
|
|
7
|
+
@imageWrapperClass={{@imageWrapperClass}}
|
|
8
|
+
@contentWrapperClass={{@contentWrapperClass}}
|
|
9
|
+
@titleClass={{@titleClass}}
|
|
10
|
+
@subtitleClass={{@subtitleClass}}
|
|
11
|
+
...attributes
|
|
12
|
+
>
|
|
13
|
+
<:image>
|
|
14
|
+
<div class="pt-1">
|
|
15
|
+
<div class="rounded-md bg-white p-1">
|
|
16
|
+
<img src={{concat "data:image/png;base64," resource.tracking_number.qr_code}} class="w-12 h-12" alt={{resource.public_id}} />
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</:image>
|
|
20
|
+
<:default>
|
|
21
|
+
<div class="text-sm font-semibold">{{n-a resource.tracking}}</div>
|
|
22
|
+
<div class="flex flex-row space-x-1">
|
|
23
|
+
<Badge @status={{@resource.status}} />
|
|
24
|
+
{{#if @resource.dispatched_at}}
|
|
25
|
+
<Badge @status="dispatched">{{concat "Dispatched at " @resource.dispatchedAt}}</Badge>
|
|
26
|
+
{{/if}}
|
|
27
|
+
</div>
|
|
28
|
+
<div class="mt-1">
|
|
29
|
+
<div class="text-xs text-gray-400 dark:text-gray-500">Date Created: {{@resource.createdAt}}</div>
|
|
30
|
+
<div class="text-xs text-gray-400 dark:text-gray-500">Type: {{smart-humanize @resource.type}}</div>
|
|
31
|
+
</div>
|
|
32
|
+
</:default>
|
|
33
|
+
</Pill>
|
|
34
|
+
{{/let}}
|
|
@@ -84,7 +84,14 @@
|
|
|
84
84
|
<label class="block uppercase tracking-wide text-xs font-medium text-gray-700 dark:text-gray-400 mb-0.5">
|
|
85
85
|
Date Range
|
|
86
86
|
</label>
|
|
87
|
-
<DatePicker
|
|
87
|
+
<DatePicker
|
|
88
|
+
@value={{this.dateFilter}}
|
|
89
|
+
@range={{true}}
|
|
90
|
+
@onSelect={{this.onDateRangeChanged}}
|
|
91
|
+
@autoClose={{true}}
|
|
92
|
+
@placeholder="Select date range"
|
|
93
|
+
class="w-full form-input form-input-sm"
|
|
94
|
+
/>
|
|
88
95
|
</div>
|
|
89
96
|
|
|
90
97
|
<div class="filter-group">
|
|
@@ -110,7 +117,6 @@
|
|
|
110
117
|
</div>
|
|
111
118
|
|
|
112
119
|
<div class="filter-actions flex items-end space-x-2">
|
|
113
|
-
<Button @icon="search" @text={{t "common.search"}} @isLoading={{this.loadPositions.isRunning}} @onClick={{perform this.loadPositions}} />
|
|
114
120
|
<Button @type="danger" @icon="times" @text={{t "common.clear"}} @disabled={{this.loadPositions.isRunning}} @onClick={{this.clearFilters}} />
|
|
115
121
|
</div>
|
|
116
122
|
</div>
|
|
@@ -120,18 +126,24 @@
|
|
|
120
126
|
<div class="replay-controls px-3 py-2">
|
|
121
127
|
<div class="flex items-center justify-between space-x-2">
|
|
122
128
|
<div class="flex items-center space-x-2">
|
|
123
|
-
<Button @type="danger" @text="Stop" @icon="stop" @size="xs" @onClick={{this.stopReplay}} />
|
|
124
|
-
<Button
|
|
125
|
-
@type="success"
|
|
126
|
-
@text="Play"
|
|
127
|
-
@icon="play"
|
|
128
|
-
@size="xs"
|
|
129
|
-
@isLoading={{this.isReplaying}}
|
|
130
|
-
@disabled={{or this.replayPositions.isRunning (not this.hasPositions)}}
|
|
131
|
-
@onClick={{this.startReplay}}
|
|
132
|
-
/>
|
|
129
|
+
<Button @type="danger" @text="Stop" @icon="stop" @size="xs" @disabled={{not (or this.isReplaying this.isPaused)}} @onClick={{this.stopReplay}} />
|
|
133
130
|
|
|
134
|
-
|
|
131
|
+
{{#if (or this.isReplaying this.isPaused)}}
|
|
132
|
+
{{#if this.isReplaying}}
|
|
133
|
+
<Button @type="warning" @text="Pause" @icon="pause" @size="xs" @onClick={{this.pauseReplay}} />
|
|
134
|
+
{{else}}
|
|
135
|
+
<Button @type="success" @text="Resume" @icon="play" @size="xs" @onClick={{this.startReplay}} />
|
|
136
|
+
{{/if}}
|
|
137
|
+
{{else}}
|
|
138
|
+
<Button @type="success" @text="Play" @icon="play" @size="xs" @disabled={{not this.hasPositions}} @onClick={{this.startReplay}} />
|
|
139
|
+
{{/if}}
|
|
140
|
+
|
|
141
|
+
<div class="step-controls flex items-center space-x-1 border-l border-gray-300 dark:border-gray-700 pl-2 ml-2">
|
|
142
|
+
<Button @type="default" @icon="backward-step" @size="xs" @disabled={{not this.hasPositions}} @onClick={{this.stepBackward}} title="Step backward" />
|
|
143
|
+
<Button @type="default" @icon="forward-step" @size="xs" @disabled={{not this.hasPositions}} @onClick={{this.stepForward}} title="Step forward" />
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="speed-control border-l border-gray-300 dark:border-gray-700 pl-2 ml-2">
|
|
135
147
|
<label class="text-xs uppercase tracking-wide font-medium text-gray-700 dark:text-gray-400 mr-1">
|
|
136
148
|
Speed:
|
|
137
149
|
</label>
|
|
@@ -146,7 +158,7 @@
|
|
|
146
158
|
</div>
|
|
147
159
|
</div>
|
|
148
160
|
|
|
149
|
-
{{#if this.isReplaying}}
|
|
161
|
+
{{#if (or this.isReplaying this.isPaused)}}
|
|
150
162
|
<div class="replay-progress flex items-center space-x-1">
|
|
151
163
|
<span class="text-sm text-gray-600 dark:text-gray-400">
|
|
152
164
|
{{this.currentReplayIndex}}/{{this.totalPositions}}
|
|
@@ -214,12 +226,6 @@
|
|
|
214
226
|
{{this.accelerationCount}}
|
|
215
227
|
</div>
|
|
216
228
|
</div>
|
|
217
|
-
<div class="metric-card">
|
|
218
|
-
<div class="metric-label truncate text-xs text-gray-500 dark:text-gray-400">Total Positions</div>
|
|
219
|
-
<div class="metric-value font-semibold text-gray-600 dark:text-gray-400">
|
|
220
|
-
{{this.totalPositions}}
|
|
221
|
-
</div>
|
|
222
|
-
</div>
|
|
223
229
|
</div>
|
|
224
230
|
</div>
|
|
225
231
|
{{/if}}
|