@fleetbase/fleetops-engine 0.6.45 → 0.6.46
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/admin/map-settings.hbs +26 -0
- package/addon/components/admin/map-settings.js +46 -0
- package/addon/components/admin/routing-settings.hbs +11 -0
- package/addon/components/cell/order-route-type.hbs +46 -0
- package/addon/components/cell/order-route-type.js +90 -0
- package/addon/components/cell/place-address.hbs +1 -0
- package/addon/components/display-place.hbs +45 -56
- package/addon/components/display-place.js +21 -0
- package/addon/components/layout/fleet-ops-sidebar/driver-listing.js +5 -5
- package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +5 -5
- package/addon/components/layout/fleet-ops-sidebar.js +8 -0
- package/addon/components/maintenance-schedule/form.js +2 -1
- package/addon/components/map/drawer/driver-listing.js +5 -5
- package/addon/components/map/drawer/place-listing.js +5 -5
- package/addon/components/map/drawer/position-listing.js +102 -21
- package/addon/components/map/drawer/vehicle-listing.js +5 -5
- package/addon/components/map/google-live-map.hbs +9 -0
- package/addon/components/map/google-live-map.js +445 -0
- package/addon/components/map/leaflet-live-map.hbs +186 -142
- package/addon/components/map/leaflet-live-map.js +121 -41
- package/addon/components/map/toolbar/visibility-control-panel.js +2 -2
- package/addon/components/map/toolbar/zones-panel.hbs +9 -9
- package/addon/components/map/toolbar.hbs +3 -3
- package/addon/components/map/toolbar.js +9 -1
- package/addon/components/modals/map-layer-form.hbs +132 -63
- package/addon/components/modals/orchestrator-import.hbs +4 -11
- package/addon/components/modals/orchestrator-import.js +17 -1
- package/addon/components/modals/update-order-activity.hbs +1 -1
- package/addon/components/orchestrator/order-pool.js +3 -2
- package/addon/components/orchestrator-workbench.js +29 -41
- package/addon/components/order/details/detail.js +2 -2
- package/addon/components/order/details/payload.hbs +5 -5
- package/addon/components/order/details/payload.js +18 -1
- package/addon/components/order/details/route.hbs +2 -2
- package/addon/components/order/details/route.js +76 -3
- package/addon/components/order/form/payload.hbs +33 -0
- package/addon/components/order/form/payload.js +1 -0
- package/addon/components/order/form/route.hbs +8 -8
- package/addon/components/order/form/route.js +78 -18
- package/addon/components/order/form.js +2 -2
- package/addon/components/order/route-editor.hbs +209 -147
- package/addon/components/order/route-editor.js +143 -102
- package/addon/components/place/address.hbs +1 -0
- package/addon/components/place/address.js +12 -0
- package/addon/components/route-list.hbs +43 -55
- package/addon/components/route-list.js +47 -0
- package/addon/components/route-optimization-wizard-panel.js +3 -4
- package/addon/components/vehicle/card.hbs +4 -4
- package/addon/controllers/maintenance/schedules/index.js +2 -1
- package/addon/controllers/operations/orders/index/details.js +110 -37
- package/addon/controllers/operations/orders/index/new.js +1 -1
- package/addon/controllers/operations/orders/index.js +8 -1
- package/addon/controllers/settings/map.js +97 -0
- package/addon/controllers/settings/routing.js +47 -12
- package/addon/extension.js +11 -0
- package/addon/helpers/place-address-html.js +6 -0
- package/addon/instance-initializers/register-google-routes.js +17 -0
- package/addon/instance-initializers/register-osrm.js +5 -0
- package/addon/routes/application.js +6 -0
- package/addon/routes/operations/orchestrator.js +13 -0
- package/addon/routes/operations/orders/index/details.js +29 -22
- package/addon/routes/operations/orders/index/new.js +9 -0
- package/addon/routes/operations/orders/index.js +0 -1
- package/addon/routes/settings/map.js +3 -0
- package/addon/routes.js +1 -0
- package/addon/services/entity-actions.js +1 -1
- package/addon/services/geofence-event-bus.js +2 -1
- package/addon/services/geofence.js +204 -168
- package/addon/services/google-routes.js +300 -0
- package/addon/services/leaflet-draw-restriction.js +2 -1
- package/addon/services/leaflet-map-manager.js +17 -0
- package/addon/services/map-adapter/google.js +2205 -0
- package/addon/services/map-adapter/leaflet.js +1042 -0
- package/addon/services/map-adapter-interface.js +551 -0
- package/addon/services/map-manager.js +768 -0
- package/addon/services/map-settings.js +75 -0
- package/addon/services/movement-tracker.js +83 -32
- package/addon/services/order-actions.js +11 -1
- package/addon/services/osrm.js +79 -6
- package/addon/services/position-playback.js +135 -62
- package/addon/services/route-engine.js +83 -0
- package/addon/services/service-areas.js +8 -1
- package/addon/styles/fleetops-engine.css +555 -0
- package/addon/templates/management/vehicles/index.hbs +0 -1
- package/addon/templates/operations/orders/index/details.hbs +0 -1
- package/addon/templates/settings/map.hbs +53 -0
- package/addon/templates/settings/routing.hbs +15 -4
- package/addon/utils/fleet-ops-options.js +1 -0
- package/addon/utils/has-valid-coordinates.js +6 -0
- package/addon/utils/map-provider-options.js +6 -0
- package/addon/utils/order-route-editing.js +86 -0
- package/addon/utils/order-route-summary.js +20 -0
- package/addon/utils/place-address-html.js +41 -0
- package/addon/utils/route-colors.js +43 -12
- package/addon/utils/route-visualization.js +93 -0
- package/addon/utils/vendor-integration.js +2 -1
- package/app/components/admin/map-settings.js +1 -0
- package/app/components/admin/routing-settings.js +1 -0
- package/app/components/cell/order-route-type.js +1 -0
- package/app/components/cell/place-address.js +1 -0
- package/app/components/map/google-live-map.js +1 -0
- package/app/components/place/address.js +1 -0
- package/app/controllers/settings/map.js +1 -0
- package/app/helpers/place-address-html.js +1 -0
- package/app/routes/settings/map.js +1 -0
- package/app/services/map-adapter/google.js +1 -0
- package/app/services/map-adapter/leaflet.js +1 -0
- package/app/services/map-adapter-interface.js +1 -0
- package/app/services/map-manager.js +1 -0
- package/app/services/map-settings.js +1 -0
- package/app/templates/settings/map.js +1 -0
- package/app/utils/has-valid-coordinates.js +1 -0
- package/app/utils/order-route-editing.js +2 -0
- package/app/utils/order-route-summary.js +1 -0
- package/app/utils/place-address-html.js +1 -0
- package/app/utils/route-colors.js +1 -1
- package/app/utils/route-visualization.js +1 -0
- package/assets/images/drawing-controls-spritesheet.svg +156 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/src/Auth/Schemas/FleetOps.php +5 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +31 -3
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +14 -1
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -0
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +3 -2
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +2 -1
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +7 -4
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +61 -34
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +3 -2
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +106 -3
- package/server/src/Http/Filter/OrderFilter.php +2 -1
- package/server/src/Http/Resources/v1/Index/Payload.php +9 -7
- package/server/src/Http/Resources/v1/Order.php +20 -1
- package/server/src/Models/Order.php +42 -1
- package/server/src/Models/OrderConfig.php +15 -0
- package/server/src/Models/Payload.php +131 -27
- package/server/src/Models/Place.php +182 -48
- package/server/src/Models/ServiceRate.php +4 -3
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +59 -23
- package/server/src/Support/Utils.php +36 -2
- package/server/src/routes.php +4 -0
- package/translations/en-us.yaml +1937 -1916
|
@@ -228,17 +228,10 @@
|
|
|
228
228
|
|
|
229
229
|
{{! Order type badge }}
|
|
230
230
|
<td>
|
|
231
|
-
{{
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
</span>
|
|
236
|
-
{{else}}
|
|
237
|
-
<span class="import-preview-badge import-preview-badge--gray">
|
|
238
|
-
<FaIcon @icon="exchange-alt" class="mr-0.5" />
|
|
239
|
-
{{t "orchestrator.col-preview-pickup-dropoff"}}
|
|
240
|
-
</span>
|
|
241
|
-
{{/if}}
|
|
231
|
+
<span class="{{group.routeSummary.badgeClass}}">
|
|
232
|
+
<FaIcon @icon={{group.routeSummary.icon}} class="mr-0.5" />
|
|
233
|
+
{{group.routeSummary.label}}
|
|
234
|
+
</span>
|
|
242
235
|
</td>
|
|
243
236
|
|
|
244
237
|
{{! Pickup }}
|
|
@@ -4,6 +4,7 @@ import { tracked } from '@glimmer/tracking';
|
|
|
4
4
|
import { action } from '@ember/object';
|
|
5
5
|
import { task } from 'ember-concurrency';
|
|
6
6
|
import * as XLSX from 'xlsx';
|
|
7
|
+
import { buildRouteTypeSummary } from '../../utils/order-route-summary';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Orchestrator Import Modal
|
|
@@ -328,7 +329,22 @@ export default class OrchestratorImportComponent extends Component {
|
|
|
328
329
|
if (!g.scheduled_at && row.scheduled_at) g.scheduled_at = row.scheduled_at;
|
|
329
330
|
});
|
|
330
331
|
|
|
331
|
-
return Array.from(groups.values())
|
|
332
|
+
return Array.from(groups.values()).map((group) => ({
|
|
333
|
+
...group,
|
|
334
|
+
routeSummary: this.routeTypeSummaryForGroup(group),
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
routeTypeSummaryForGroup(group) {
|
|
339
|
+
const baseSummary = buildRouteTypeSummary({
|
|
340
|
+
hasIntermediateWaypoints: group.order_type === 'multi_waypoint' || group.waypointCount > 0,
|
|
341
|
+
intermediateStopCount: group.waypointCount,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
...baseSummary,
|
|
346
|
+
label: this.intl.t(baseSummary.translationKey, baseSummary.translationOptions),
|
|
347
|
+
};
|
|
332
348
|
}
|
|
333
349
|
|
|
334
350
|
// ── Footer action buttons (injected into modal footer via modalsManager) ──
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
</div>
|
|
66
66
|
<label for="activity_custom" class="ml-3 flex flex-col cursor-pointer w-full pr-5">
|
|
67
67
|
<div class="font-semibold text-sm mb-1">Add custom activity</div>
|
|
68
|
-
<span class="block text-sm font-medium">
|
|
68
|
+
<span class="block text-sm font-medium mb-0.5">
|
|
69
69
|
<Input
|
|
70
70
|
@type="text"
|
|
71
71
|
@value={{this.customActivity.status}}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { isArray } from '@ember/array';
|
|
3
4
|
import { action } from '@ember/object';
|
|
4
5
|
import { inject as service } from '@ember/service';
|
|
5
6
|
|
|
@@ -157,7 +158,7 @@ export default class OrchestratorOrderPoolComponent extends Component {
|
|
|
157
158
|
stops.push(payload.pickup.address ?? payload.pickup.street1 ?? payload.pickup.name ?? '—');
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
const waypoints = typeof payload.waypoints?.toArray === 'function' ? payload.waypoints.toArray() :
|
|
161
|
+
const waypoints = typeof payload.waypoints?.toArray === 'function' ? payload.waypoints.toArray() : isArray(payload.waypoints) ? payload.waypoints : [];
|
|
161
162
|
|
|
162
163
|
for (const wp of waypoints) {
|
|
163
164
|
const place = wp.place ?? wp;
|
|
@@ -318,7 +319,7 @@ export default class OrchestratorOrderPoolComponent extends Component {
|
|
|
318
319
|
let value = null;
|
|
319
320
|
|
|
320
321
|
const cfvCollection = order.custom_field_values ?? order.customFieldValues ?? [];
|
|
321
|
-
const cfvArray = typeof cfvCollection?.toArray === 'function' ? cfvCollection.toArray() :
|
|
322
|
+
const cfvArray = typeof cfvCollection?.toArray === 'function' ? cfvCollection.toArray() : isArray(cfvCollection) ? cfvCollection : [];
|
|
322
323
|
|
|
323
324
|
for (const cfv of cfvArray) {
|
|
324
325
|
const cf = cfv.custom_field ?? (typeof cfv.customField?.get === 'function' ? cfv.customField : null) ?? cfv.customField ?? null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
|
+
import { isArray } from '@ember/array';
|
|
2
3
|
import { inject as service } from '@ember/service';
|
|
3
4
|
import { tracked } from '@glimmer/tracking';
|
|
4
5
|
import { action } from '@ember/object';
|
|
@@ -6,8 +7,6 @@ import { later } from '@ember/runloop';
|
|
|
6
7
|
import { debug } from '@ember/debug';
|
|
7
8
|
import { task } from 'ember-concurrency';
|
|
8
9
|
import { colorForId, waypointIconHtml } from '../utils/route-colors';
|
|
9
|
-
import polyline from '@fleetbase/ember-core/utils/polyline';
|
|
10
|
-
import getRoutingHost from '@fleetbase/ember-core/utils/get-routing-host';
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* OrchestratorWorkbenchComponent
|
|
@@ -36,7 +35,8 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
36
35
|
@service intl;
|
|
37
36
|
@service modalsManager;
|
|
38
37
|
@service location;
|
|
39
|
-
@service
|
|
38
|
+
@service mapManager;
|
|
39
|
+
@service routeEngine;
|
|
40
40
|
@service('order-allocation') allocationService;
|
|
41
41
|
|
|
42
42
|
/** Routing controls added to the map for the current proposed plan. */
|
|
@@ -152,6 +152,10 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
152
152
|
this.loadCardFields.perform();
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
willDestroy() {
|
|
156
|
+
super.willDestroy(...arguments);
|
|
157
|
+
}
|
|
158
|
+
|
|
155
159
|
// ── Data loading ──────────────────────────────────────────────────────────
|
|
156
160
|
|
|
157
161
|
@task *loadData() {
|
|
@@ -536,10 +540,8 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
536
540
|
|
|
537
541
|
@action onMapLoad({ target: map }) {
|
|
538
542
|
this.leafletMap = map;
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
// routing controls silently time out.
|
|
542
|
-
this.leafletMapManager.setMap(map);
|
|
543
|
+
this.mapManager.setActiveProvider('leaflet');
|
|
544
|
+
this.mapManager.setMapInstance(map);
|
|
543
545
|
// If orders have already loaded before the map mounted, re-center now
|
|
544
546
|
// using the same fitBounds strategy as _centerMapOnOrders().
|
|
545
547
|
if (this._mapCenteredOnOrders) {
|
|
@@ -563,37 +565,28 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
563
565
|
*/
|
|
564
566
|
async _drawRoutingControls() {
|
|
565
567
|
this._clearRoutingControls();
|
|
566
|
-
const map = this.leafletMap;
|
|
567
|
-
if (!map) return;
|
|
568
568
|
const groups = this.planByVehicle;
|
|
569
569
|
if (!groups.length) return;
|
|
570
|
-
|
|
571
|
-
const routingHost = getRoutingHost();
|
|
572
570
|
const allLatLngs = [];
|
|
573
571
|
|
|
574
572
|
for (const group of groups) {
|
|
575
573
|
const waypoints = group.routeWaypoints;
|
|
576
574
|
if (!waypoints || waypoints.length < 2) continue;
|
|
577
575
|
try {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (!latlngs.length) continue;
|
|
589
|
-
const pl = L.polyline(latlngs, {
|
|
590
|
-
color: group.routeColor,
|
|
591
|
-
weight: 4,
|
|
592
|
-
opacity: 0.85,
|
|
576
|
+
const routeHandle = await this.mapManager.addRoutingControl(waypoints, {
|
|
577
|
+
engine: this.routeEngine.getDisplayEngine('osrm'),
|
|
578
|
+
suppressMarkers: true,
|
|
579
|
+
position: false,
|
|
580
|
+
tag: `orchestrator:${group.vehicle?.public_id ?? group.vehicleId ?? group.vehicle_uuid ?? 'unknown'}`,
|
|
581
|
+
polylineOptions: {
|
|
582
|
+
color: group.routeColor,
|
|
583
|
+
weight: 4,
|
|
584
|
+
opacity: 0.85,
|
|
585
|
+
},
|
|
593
586
|
});
|
|
594
|
-
|
|
595
|
-
this._routingControls.push(
|
|
596
|
-
allLatLngs.push(...
|
|
587
|
+
if (!routeHandle?.route?.coordinates?.length) continue;
|
|
588
|
+
this._routingControls.push(routeHandle);
|
|
589
|
+
allLatLngs.push(...routeHandle.route.coordinates);
|
|
597
590
|
} catch {
|
|
598
591
|
// Non-fatal: routing may fail for individual vehicles if OSRM
|
|
599
592
|
// cannot find a route (e.g. ferry-only legs, missing road data)
|
|
@@ -603,7 +596,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
603
596
|
// Fit the map to show all drawn routes.
|
|
604
597
|
if (allLatLngs.length >= 2) {
|
|
605
598
|
try {
|
|
606
|
-
|
|
599
|
+
this.mapManager.fitBounds(allLatLngs, { padding: [40, 40], maxZoom: 14 });
|
|
607
600
|
} catch {
|
|
608
601
|
// ignore fitBounds errors
|
|
609
602
|
}
|
|
@@ -614,14 +607,9 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
614
607
|
* Remove all route polylines that were added for the current plan.
|
|
615
608
|
*/
|
|
616
609
|
_clearRoutingControls() {
|
|
617
|
-
const
|
|
618
|
-
for (const layer of this._routingControls) {
|
|
610
|
+
for (const routeHandle of this._routingControls) {
|
|
619
611
|
try {
|
|
620
|
-
|
|
621
|
-
map.removeLayer(layer);
|
|
622
|
-
} else {
|
|
623
|
-
layer.remove?.();
|
|
624
|
-
}
|
|
612
|
+
this.mapManager.removeRoutingControl(routeHandle);
|
|
625
613
|
} catch {
|
|
626
614
|
// ignore removal errors
|
|
627
615
|
}
|
|
@@ -686,7 +674,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
686
674
|
}
|
|
687
675
|
|
|
688
676
|
get hasProposedPlan() {
|
|
689
|
-
return
|
|
677
|
+
return isArray(this.proposedPlan) && this.proposedPlan.length > 0;
|
|
690
678
|
}
|
|
691
679
|
|
|
692
680
|
get hasUnassigned() {
|
|
@@ -792,7 +780,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
792
780
|
if (loc) {
|
|
793
781
|
// GeoJSON Point — with or without the type field.
|
|
794
782
|
// LaravelMysqlSpatial SpatialExpression may serialize without type.
|
|
795
|
-
if (
|
|
783
|
+
if (isArray(loc.coordinates) && loc.coordinates.length >= 2) {
|
|
796
784
|
const lng = parseFloat(loc.coordinates[0]);
|
|
797
785
|
const lat = parseFloat(loc.coordinates[1]);
|
|
798
786
|
// Use isFinite to accept 0 as a valid coordinate (not just truthy)
|
|
@@ -809,7 +797,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
809
797
|
}
|
|
810
798
|
}
|
|
811
799
|
// Numeric array [lat, lng] (some internal formats)
|
|
812
|
-
if (
|
|
800
|
+
if (isArray(loc) && loc.length >= 2) {
|
|
813
801
|
const lat = parseFloat(loc[0]);
|
|
814
802
|
const lng = parseFloat(loc[1]);
|
|
815
803
|
if (isFinite(lat) && isFinite(lng) && (lat !== 0 || lng !== 0)) {
|
|
@@ -848,7 +836,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
848
836
|
// NOTE: isMultiDrop is an Ember Data computed property and won't exist on
|
|
849
837
|
// plain JSON objects returned by the orchestrator/orders endpoint.
|
|
850
838
|
const waypoints = payload.waypoints;
|
|
851
|
-
const hasWaypoints =
|
|
839
|
+
const hasWaypoints = isArray(waypoints) && waypoints.length > 0;
|
|
852
840
|
const isMultiDrop = payload.isMultiDrop === true || (hasWaypoints && !payload.pickup && !payload.dropoff);
|
|
853
841
|
if (isMultiDrop && hasWaypoints) {
|
|
854
842
|
const sorted = [...waypoints].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
@@ -5,7 +5,7 @@ import { action } from '@ember/object';
|
|
|
5
5
|
export default class OrderDetailsDetailComponent extends Component {
|
|
6
6
|
@service orderActions;
|
|
7
7
|
@service driverActions;
|
|
8
|
-
@service
|
|
8
|
+
@service mapManager;
|
|
9
9
|
@service intl;
|
|
10
10
|
|
|
11
11
|
get actionButtons() {
|
|
@@ -37,6 +37,6 @@ export default class OrderDetailsDetailComponent extends Component {
|
|
|
37
37
|
|
|
38
38
|
@action focusOrderAssignedDriver(driver) {
|
|
39
39
|
this.driverActions.panel.view(driver);
|
|
40
|
-
this.
|
|
40
|
+
this.mapManager.focusResource(driver, 18);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<ContentPanel @title={{t "order.fields.payload"}} @open={{true}} @actionButtons={{this.actionButtons}} @wrapperClass="bordered-top">
|
|
2
|
-
{{#if @resource.
|
|
2
|
+
{{#if @resource.hasIntermediateWaypoints}}
|
|
3
3
|
<div class="space-y-2">
|
|
4
4
|
{{#each @resource.entitiesByDestination as |group|}}
|
|
5
5
|
<div class="rounded-md border border-gray-200 dark:border-gray-900 p-3">
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
</div>
|
|
26
26
|
</div>
|
|
27
27
|
{{/if}}
|
|
28
|
-
<DisplayPlace @place={{group.waypoint}} @addressClass="text-sm" />
|
|
29
|
-
<div class="
|
|
30
|
-
<
|
|
28
|
+
<DisplayPlace @place={{group.waypoint}} @addressClass="text-sm" @hideBadges={{true}} />
|
|
29
|
+
<div class="flex align-self mt-1">
|
|
30
|
+
<ClickToCopy @value={{group.waypoint.tracking}} class="rounded-md bg-yellow-300 text-yellow-900 px-2 py-1 text-sm">{{t "order.fields.tracking"}}
|
|
31
31
|
{{group.waypoint.tracking}}
|
|
32
|
-
</
|
|
32
|
+
</ClickToCopy>
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
35
35
|
<div>
|
|
@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
|
|
|
3
3
|
import { action } from '@ember/object';
|
|
4
4
|
import { debug } from '@ember/debug';
|
|
5
5
|
import { task } from 'ember-concurrency';
|
|
6
|
+
import placeAddressHtml from '../../../utils/place-address-html';
|
|
6
7
|
|
|
7
8
|
export default class OrderDetailsPayloadComponent extends Component {
|
|
8
9
|
@service entityActions;
|
|
@@ -13,7 +14,23 @@ export default class OrderDetailsPayloadComponent extends Component {
|
|
|
13
14
|
@service intl;
|
|
14
15
|
|
|
15
16
|
get actionButtons() {
|
|
16
|
-
if (this.args.resource.
|
|
17
|
+
if (this.args.resource.hasIntermediateWaypoints) {
|
|
18
|
+
const places = this.args.resource.places ?? [];
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
type: 'default',
|
|
22
|
+
text: this.intl.t('order.actions.add-entity'),
|
|
23
|
+
icon: 'plus',
|
|
24
|
+
iconPrefix: 'fas',
|
|
25
|
+
permission: 'fleet-ops update order',
|
|
26
|
+
disabled: this.args.resource.status === 'canceled',
|
|
27
|
+
items: places.map((p) => ({
|
|
28
|
+
text: placeAddressHtml(p),
|
|
29
|
+
onClick: () => this.addEntity.perform(p),
|
|
30
|
+
})),
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
}
|
|
17
34
|
|
|
18
35
|
return [
|
|
19
36
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<ContentPanel @title={{t "order.fields.route-panel-title"}} @open={{true}} @actionButtons={{this.actionButtons}} @wrapperClass="bordered-top">
|
|
2
|
-
{{#if @resource.
|
|
2
|
+
{{#if @resource.hasIntermediateWaypoints}}
|
|
3
3
|
<div>
|
|
4
4
|
<RouteList @order={{@resource}} @eta={{@resource.eta}} @waypointActions={{hash viewWaypointLabel=(hash label=(t "order.fields.get-label") fn=this.viewWaypointLabel)}} />
|
|
5
5
|
</div>
|
|
@@ -19,4 +19,4 @@
|
|
|
19
19
|
</div>
|
|
20
20
|
</div>
|
|
21
21
|
{{/if}}
|
|
22
|
-
</ContentPanel>
|
|
22
|
+
</ContentPanel>
|
|
@@ -2,14 +2,38 @@ import Component from '@glimmer/component';
|
|
|
2
2
|
import { inject as service } from '@ember/service';
|
|
3
3
|
import { action } from '@ember/object';
|
|
4
4
|
import { debug } from '@ember/debug';
|
|
5
|
+
import { task } from 'ember-concurrency';
|
|
6
|
+
import { applyOptimizedIntermediateWaypoints, buildRouteOptimizationInput, canOptimizeIntermediateWaypoints } from '../../../utils/order-route-editing';
|
|
5
7
|
|
|
6
8
|
export default class OrderDetailsRouteComponent extends Component {
|
|
7
9
|
@service orderActions;
|
|
8
10
|
@service modalsManager;
|
|
9
11
|
@service fetch;
|
|
12
|
+
@service notifications;
|
|
13
|
+
@service routeEngine;
|
|
14
|
+
@service routeOptimization;
|
|
15
|
+
@service hostRouter;
|
|
16
|
+
@service intl;
|
|
10
17
|
|
|
11
18
|
get actionButtons() {
|
|
12
19
|
return [
|
|
20
|
+
this.showOptimizationSelect
|
|
21
|
+
? {
|
|
22
|
+
component: 'route-optimization-engine-select-button',
|
|
23
|
+
onClick: (service) => this.optimizeRouteWithService.perform(service),
|
|
24
|
+
isLoading: this.optimizeRouteWithService.isRunning,
|
|
25
|
+
disabled: !this.canOptimizeRoute,
|
|
26
|
+
}
|
|
27
|
+
: {
|
|
28
|
+
type: 'magic',
|
|
29
|
+
icon: 'magic',
|
|
30
|
+
text: this.intl.t('order.fields.optimize-route'),
|
|
31
|
+
onClick: () => this.optimizeRoute.perform(),
|
|
32
|
+
permission: 'fleet-ops optimize order',
|
|
33
|
+
helpText: this.intl.t('order.fields.optimize-route-help-text'),
|
|
34
|
+
disabled: !this.canOptimizeRoute,
|
|
35
|
+
isLoading: this.optimizeRoute.isRunning,
|
|
36
|
+
},
|
|
13
37
|
{
|
|
14
38
|
type: 'default',
|
|
15
39
|
text: 'Edit',
|
|
@@ -24,8 +48,59 @@ export default class OrderDetailsRouteComponent extends Component {
|
|
|
24
48
|
];
|
|
25
49
|
}
|
|
26
50
|
|
|
51
|
+
get canOptimizeRoute() {
|
|
52
|
+
return canOptimizeIntermediateWaypoints(this.args.resource?.payload);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get showOptimizationSelect() {
|
|
56
|
+
return this.routeOptimization.availableEngines.length > 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@task *optimizeRouteWithService(service) {
|
|
60
|
+
const order = this.args.resource;
|
|
61
|
+
const optimizationInput = buildRouteOptimizationInput(order);
|
|
62
|
+
|
|
63
|
+
if (!service) {
|
|
64
|
+
this.notifications.error('No route optimization engine was selected.');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const result = yield this.routeOptimization.optimize(service, {
|
|
70
|
+
context: 'order_details_route',
|
|
71
|
+
...optimizationInput,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
yield this.persistOptimizedRoute(result);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
this.notifications.error(err.message ?? this.intl.t('fleet-ops.operations.orders.index.new.route-error'));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@task *optimizeRoute() {
|
|
81
|
+
const order = this.args.resource;
|
|
82
|
+
const optimizationInput = buildRouteOptimizationInput(order);
|
|
83
|
+
const service = this.routeEngine.getOptimizationEngine('osrm');
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result = yield this.routeOptimization.optimize(service, {
|
|
87
|
+
context: 'order_details_route',
|
|
88
|
+
...optimizationInput,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
yield this.persistOptimizedRoute(result);
|
|
92
|
+
} catch (_err) {
|
|
93
|
+
this.notifications.error(this.intl.t('fleet-ops.operations.orders.index.new.route-error'));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
*persistOptimizedRoute({ sortedWaypoints }) {
|
|
98
|
+
applyOptimizedIntermediateWaypoints(this.args.resource.payload, sortedWaypoints);
|
|
99
|
+
yield this.orderActions.saveRoute.perform(this.args.resource);
|
|
100
|
+
yield this.hostRouter.refresh();
|
|
101
|
+
}
|
|
102
|
+
|
|
27
103
|
@action async viewWaypointLabel(waypoint) {
|
|
28
|
-
// render dialog to display label within
|
|
29
104
|
this.modalsManager.show(`modals/order-label`, {
|
|
30
105
|
title: 'Waypoint Label',
|
|
31
106
|
modalClass: 'modal-xl',
|
|
@@ -34,14 +109,12 @@ export default class OrderDetailsRouteComponent extends Component {
|
|
|
34
109
|
});
|
|
35
110
|
|
|
36
111
|
try {
|
|
37
|
-
// load the pdf label from base64
|
|
38
112
|
// eslint-disable-next-line no-undef
|
|
39
113
|
const fileReader = new FileReader();
|
|
40
114
|
const { data: pdfStream } = await this.fetch.get(`orders/label/${waypoint.waypoint_public_id}?format=base64`);
|
|
41
115
|
// eslint-disable-next-line no-undef
|
|
42
116
|
const base64 = await fetch(`data:application/pdf;base64,${pdfStream}`);
|
|
43
117
|
const blob = await base64.blob();
|
|
44
|
-
// load into file reader
|
|
45
118
|
fileReader.onload = (event) => {
|
|
46
119
|
const data = event.target.result;
|
|
47
120
|
this.modalsManager.setOption('data', data);
|
|
@@ -71,6 +71,24 @@
|
|
|
71
71
|
<div>
|
|
72
72
|
<Input @value={{entity.sku}} @type="text" class="w-full form-input form-input-sm" placeholder={{t "order.fields.sku"}} />
|
|
73
73
|
</div>
|
|
74
|
+
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
|
|
75
|
+
<PowerSelect
|
|
76
|
+
@options={{get-fleet-ops-options "entityTypes"}}
|
|
77
|
+
@selected={{find-by "value" entity.type (get-fleet-ops-options "entityTypes")}}
|
|
78
|
+
@onChange={{set-model-attr entity "type"}}
|
|
79
|
+
@placeholder="Select entity type"
|
|
80
|
+
@triggerClass="form-select"
|
|
81
|
+
as |type|
|
|
82
|
+
>
|
|
83
|
+
<div class="text-sm">
|
|
84
|
+
<div class="font-semibold normalize-in-trigger">{{type.label}}</div>
|
|
85
|
+
<div class="hide-from-trigger">{{type.description}}</div>
|
|
86
|
+
</div>
|
|
87
|
+
</PowerSelect>
|
|
88
|
+
</div>
|
|
89
|
+
<RegistryYield @registry="fleet-ops:component:order:form:payload:entity:form" as |RegistryComponent|>
|
|
90
|
+
<RegistryComponent @entity={{entity}} @order={{@resource}} @controller={{this}} />
|
|
91
|
+
</RegistryYield>
|
|
74
92
|
<Button @icon="pencil" @text={{t "order.fields.edit-item"}} @size="sm" @onClick={{fn this.editEntity entity}} />
|
|
75
93
|
</div>
|
|
76
94
|
<div class="absolute top-0 right-0 p-2">
|
|
@@ -105,6 +123,21 @@
|
|
|
105
123
|
<div>
|
|
106
124
|
<Input @value={{entity.sku}} @type="text" class="w-full form-input form-input-sm" placeholder={{t "order.fields.sku"}} />
|
|
107
125
|
</div>
|
|
126
|
+
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
|
|
127
|
+
<PowerSelect
|
|
128
|
+
@options={{get-fleet-ops-options "entityTypes"}}
|
|
129
|
+
@selected={{find-by "value" entity.type (get-fleet-ops-options "entityTypes")}}
|
|
130
|
+
@onChange={{set-model-attr entity "type"}}
|
|
131
|
+
@placeholder="Select entity type"
|
|
132
|
+
@triggerClass="form-select"
|
|
133
|
+
as |type|
|
|
134
|
+
>
|
|
135
|
+
<div class="text-sm">
|
|
136
|
+
<div class="font-semibold normalize-in-trigger">{{type.label}}</div>
|
|
137
|
+
<div class="hide-from-trigger">{{type.description}}</div>
|
|
138
|
+
</div>
|
|
139
|
+
</PowerSelect>
|
|
140
|
+
</div>
|
|
108
141
|
<RegistryYield @registry="fleet-ops:component:order:form:payload:entity:form" as |RegistryComponent|>
|
|
109
142
|
<RegistryComponent @entity={{entity}} @order={{@resource}} @controller={{this}} />
|
|
110
143
|
</RegistryYield>
|
|
@@ -52,6 +52,7 @@ export default class OrderFormPayloadComponent extends Component {
|
|
|
52
52
|
@action addEntity(importId = null) {
|
|
53
53
|
const entity = this.store.createRecord('entity', {
|
|
54
54
|
_import_id: typeof importId === 'string' ? importId : null,
|
|
55
|
+
type: 'entity',
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
this.args.resource.payload.entities.pushObject(entity);
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
@renderInPlace={{true}}
|
|
57
57
|
@onChange={{fn this.setWaypointPlace index}}
|
|
58
58
|
@permission={{cannot-write @resource}}
|
|
59
|
-
as |
|
|
59
|
+
as |place|
|
|
60
60
|
>
|
|
61
|
-
{{
|
|
61
|
+
<Place::Address @place={{place}} />
|
|
62
62
|
</ModelSelect>
|
|
63
63
|
{{#if (and @resource.facilitator.isIntegratedVendor (is-not-facilitator-supported-place @resource.facilitator waypoint.place))}}
|
|
64
64
|
<InfoBlock
|
|
@@ -169,9 +169,9 @@
|
|
|
169
169
|
@renderInPlace={{true}}
|
|
170
170
|
@onChange={{fn this.setPayloadPlace "pickup"}}
|
|
171
171
|
@permission={{cannot-write @resource}}
|
|
172
|
-
as |
|
|
172
|
+
as |place|
|
|
173
173
|
>
|
|
174
|
-
{{
|
|
174
|
+
<Place::Address @place={{place}} />
|
|
175
175
|
</ModelSelect>
|
|
176
176
|
|
|
177
177
|
{{#if (and @resource.facilitator.isIntegratedVendor (is-not-facilitator-supported-place @resource.facilitator @resource.payload.pickup))}}
|
|
@@ -213,9 +213,9 @@
|
|
|
213
213
|
@renderInPlace={{true}}
|
|
214
214
|
@onChange={{fn this.setPayloadPlace "dropoff"}}
|
|
215
215
|
@permission={{cannot-write @resource}}
|
|
216
|
-
as |
|
|
216
|
+
as |place|
|
|
217
217
|
>
|
|
218
|
-
{{
|
|
218
|
+
<Place::Address @place={{place}} />
|
|
219
219
|
</ModelSelect>
|
|
220
220
|
|
|
221
221
|
{{#if (and @resource.facilitator.isIntegratedVendor (is-not-facilitator-supported-place @resource.facilitator @resource.payload.dropoff))}}
|
|
@@ -250,9 +250,9 @@
|
|
|
250
250
|
@renderInPlace={{true}}
|
|
251
251
|
@onChange={{fn this.setPayloadPlace "return"}}
|
|
252
252
|
@permission={{cannot-write @resource}}
|
|
253
|
-
as |
|
|
253
|
+
as |place|
|
|
254
254
|
>
|
|
255
|
-
{{
|
|
255
|
+
<Place::Address @place={{place}} />
|
|
256
256
|
</ModelSelect>
|
|
257
257
|
|
|
258
258
|
{{#if (and @resource.facilitator.isIntegratedVendor (is-not-facilitator-supported-place @resource.facilitator @resource.payload.return))}}
|