@fleetbase/fleetops-engine 0.6.46 → 0.6.48
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/routing-settings.hbs +71 -0
- package/addon/components/admin/routing-settings.js +124 -0
- package/addon/components/customer/orders.js +1 -1
- package/addon/components/display-place.hbs +1 -1
- package/addon/components/map/order-list-overlay/order.hbs +18 -3
- package/addon/components/map/toolbar.hbs +2 -2
- package/addon/components/map/toolbar.js +10 -2
- package/addon/components/orchestrator/phase-builder.hbs +28 -5
- package/addon/components/orchestrator/phase-builder.js +21 -1
- package/addon/components/orchestrator/resource-panel.hbs +19 -1
- package/addon/components/orchestrator/resource-panel.js +4 -0
- package/addon/components/orchestrator-workbench.js +3 -1
- package/addon/components/order/details/purchase-rate.hbs +19 -22
- package/addon/components/order/details/route.hbs +26 -1
- package/addon/components/order/details/route.js +50 -0
- package/addon/components/order/details/tracking.hbs +171 -27
- package/addon/components/order/details/tracking.js +457 -0
- package/addon/components/order/form/details.hbs +1 -2
- package/addon/components/order/form/details.js +6 -0
- package/addon/components/order/form/route.js +9 -1
- package/addon/components/order/kanban-card.hbs +18 -3
- package/addon/components/order-list-overlay/order.hbs +18 -3
- package/addon/components/order-progress-card.hbs +21 -6
- package/addon/components/order-tracking-lookup.hbs +7 -7
- package/addon/components/order-tracking-lookup.js +1 -1
- package/addon/components/route-list.hbs +78 -8
- package/addon/components/route-list.js +48 -6
- package/addon/components/tracking-stop-progress.hbs +45 -0
- package/addon/components/tracking-stop-progress.js +79 -0
- package/addon/controllers/settings/payments/index.js +1 -1
- package/addon/controllers/settings/routing.js +98 -1
- package/addon/helpers/format-duration.js +38 -0
- package/addon/routes/application.js +6 -0
- package/addon/services/order-list-overlay.js +2 -2
- package/addon/styles/fleetops-engine.css +733 -0
- package/addon/templates/settings/routing.hbs +60 -2
- package/app/components/tracking-stop-progress.js +1 -0
- package/app/helpers/format-duration.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/pnpm-workspace.yaml +7 -0
- package/server/config/fleetops.php +32 -11
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
- package/server/seeders/VroomOrchestrationSeeder.php +402 -0
- package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +69 -15
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +119 -12
- package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
- package/server/src/Models/Contact.php +122 -13
- package/server/src/Models/Order.php +1 -1
- package/server/src/Models/Payload.php +8 -8
- package/server/src/Models/Place.php +0 -3
- package/server/src/Models/ServiceRate.php +1 -1
- package/server/src/Observers/ContactObserver.php +8 -0
- package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +330 -38
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
- package/server/src/Providers/FleetOpsServiceProvider.php +27 -0
- package/server/src/Support/LiveOrderQuery.php +75 -0
- package/server/src/Support/Metrics.php +15 -0
- package/server/src/Support/OSRM.php +11 -6
- package/server/src/Support/OrderTracker.php +7 -582
- package/server/src/Support/Utils.php +1 -1
- package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
- package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
- package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
- package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
- package/server/src/Tracking/README.md +25 -0
- package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
- package/server/src/Tracking/TrackingContext.php +63 -0
- package/server/src/Tracking/TrackingContextBuilder.php +204 -0
- package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
- package/server/src/Tracking/TrackingOptions.php +49 -0
- package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
- package/server/src/Tracking/TrackingProviderManager.php +89 -0
- package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
- package/server/src/Tracking/TrackingProviderResult.php +20 -0
- package/server/src/Tracking/TrackingStop.php +59 -0
- package/server/src/routes.php +11 -0
- package/server/tests/CapacityAllocationEngineTest.php +108 -0
- package/server/tests/LiveOrderQueryTest.php +34 -0
- package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
- package/server/tests/OrchestratorConsumableApiTest.php +74 -0
- package/server/tests/PingDriverEndpointTest.php +19 -0
- package/server/tests/TrackingIntelligenceTest.php +176 -0
- package/server/tests/VroomOrchestrationEngineTest.php +159 -0
- package/translations/en-us.yaml +4 -0
|
@@ -3,9 +3,80 @@
|
|
|
3
3
|
Configure system-wide routing engine defaults here. Organizations can optionally override these values from their own routing settings.
|
|
4
4
|
</InfoBlock>
|
|
5
5
|
|
|
6
|
+
<ContentPanel @title="Tracking Defaults" @open={{true}} @wrapperClass="bordered-classic">
|
|
7
|
+
<InputGroup @name="Default Tracking Provider" @helpText="Default provider for organization-level tracking intelligence unless an organization overrides it.">
|
|
8
|
+
<Select
|
|
9
|
+
class="w-full"
|
|
10
|
+
@value={{this.trackingProvider}}
|
|
11
|
+
@options={{this.trackingProviderOptions}}
|
|
12
|
+
@optionLabel="label"
|
|
13
|
+
@optionValue="value"
|
|
14
|
+
@placeholder="Select default tracking provider"
|
|
15
|
+
@onSelect={{fn (mut this.trackingProvider)}}
|
|
16
|
+
/>
|
|
17
|
+
</InputGroup>
|
|
18
|
+
<InputGroup @name="Default Fallback Providers" @helpText="Providers used when the selected tracking provider is unavailable.">
|
|
19
|
+
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
|
|
20
|
+
<PowerSelectMultiple
|
|
21
|
+
@searchEnabled={{true}}
|
|
22
|
+
@options={{this.trackingProviderOptions}}
|
|
23
|
+
@selected={{this.selectedTrackingFallbackOptions}}
|
|
24
|
+
@onChange={{this.setTrackingFallbacks}}
|
|
25
|
+
@placeholder="Select default fallback providers"
|
|
26
|
+
@triggerClass="form-select form-input form-input-sm flex-1"
|
|
27
|
+
as |provider|
|
|
28
|
+
>
|
|
29
|
+
{{provider.label}}
|
|
30
|
+
</PowerSelectMultiple>
|
|
31
|
+
</div>
|
|
32
|
+
</InputGroup>
|
|
33
|
+
<div class="flex justify-end">
|
|
34
|
+
<Button
|
|
35
|
+
@type="default"
|
|
36
|
+
@size="xs"
|
|
37
|
+
@icon={{if this.showTrackingAdvancedSettings "chevron-up" "chevron-down"}}
|
|
38
|
+
@text="Advanced Settings"
|
|
39
|
+
@onClick={{this.toggleTrackingAdvancedSettings}}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
{{#if this.showTrackingAdvancedSettings}}
|
|
43
|
+
<div class="mt-4 space-y-4 rounded-md border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
|
|
44
|
+
<InputGroup @name="Traffic-Aware ETA" @helpText="Use traffic-aware provider calculations when supported.">
|
|
45
|
+
<Toggle @isToggled={{this.trackingTrafficEnabled}} @onToggle={{fn (mut this.trackingTrafficEnabled)}} />
|
|
46
|
+
</InputGroup>
|
|
47
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
48
|
+
<InputGroup @name="Summary Cache TTL">
|
|
49
|
+
<Input @type="number" @value={{this.trackingCacheTtlSeconds}} min="15" max="300" class="form-input w-full" />
|
|
50
|
+
</InputGroup>
|
|
51
|
+
<InputGroup @name="Route Cache TTL">
|
|
52
|
+
<Input @type="number" @value={{this.trackingRouteCacheTtlSeconds}} min="60" max="1800" class="form-input w-full" />
|
|
53
|
+
</InputGroup>
|
|
54
|
+
<InputGroup @name="Stale Location Threshold">
|
|
55
|
+
<Input @type="number" @value={{this.trackingStaleLocationThresholdSeconds}} min="30" max="3600" class="form-input w-full" />
|
|
56
|
+
</InputGroup>
|
|
57
|
+
<InputGroup @name="Fallback Speed">
|
|
58
|
+
<Input @type="number" @value={{this.trackingDefaultVehicleSpeedKph}} min="1" max="160" class="form-input w-full" />
|
|
59
|
+
</InputGroup>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
{{/if}}
|
|
63
|
+
</ContentPanel>
|
|
64
|
+
|
|
6
65
|
<RegistryYield @registry="fleet-ops:component:admin:routing-settings" as |RegistryComponent|>
|
|
7
66
|
<div>
|
|
8
67
|
<RegistryComponent />
|
|
9
68
|
</div>
|
|
10
69
|
</RegistryYield>
|
|
11
70
|
</div>
|
|
71
|
+
|
|
72
|
+
<EmberWormhole @to="next-view-section-subheader-actions">
|
|
73
|
+
<Button
|
|
74
|
+
@type="primary"
|
|
75
|
+
@size="sm"
|
|
76
|
+
@icon="save"
|
|
77
|
+
@text="Save Changes"
|
|
78
|
+
@onClick={{perform this.saveSettings}}
|
|
79
|
+
@isLoading={{or this.saveSettings.isRunning this.loadSettings.isRunning}}
|
|
80
|
+
@disabled={{this.saveSettings.isRunning}}
|
|
81
|
+
/>
|
|
82
|
+
</EmberWormhole>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
import { action } from '@ember/object';
|
|
5
|
+
import { task } from 'ember-concurrency';
|
|
6
|
+
|
|
7
|
+
export default class AdminRoutingSettingsComponent extends Component {
|
|
8
|
+
@service fetch;
|
|
9
|
+
@service notifications;
|
|
10
|
+
@tracked trackingProvider = 'google_routes';
|
|
11
|
+
@tracked trackingFallbacks = ['osrm', 'calculated'];
|
|
12
|
+
@tracked trackingTrafficEnabled = true;
|
|
13
|
+
@tracked trackingCacheTtlSeconds = 60;
|
|
14
|
+
@tracked trackingRouteCacheTtlSeconds = 600;
|
|
15
|
+
@tracked trackingStaleLocationThresholdSeconds = 300;
|
|
16
|
+
@tracked trackingDefaultVehicleSpeedKph = 35;
|
|
17
|
+
@tracked showTrackingAdvancedSettings = false;
|
|
18
|
+
@tracked trackingProviderOptions = [
|
|
19
|
+
{ value: 'google_routes', label: 'Google Routes' },
|
|
20
|
+
{ value: 'osrm', label: 'OSRM' },
|
|
21
|
+
{ value: 'calculated', label: 'Calculated' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super(...arguments);
|
|
26
|
+
this.loadSettings.perform();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@task *loadSettings() {
|
|
30
|
+
try {
|
|
31
|
+
const settings = yield this.fetch.get('fleet-ops/settings/admin-tracking-settings');
|
|
32
|
+
this.trackingProvider = settings.provider ?? 'google_routes';
|
|
33
|
+
this.trackingFallbacks = this.normalizeFallbacks(settings.fallbacks);
|
|
34
|
+
this.trackingTrafficEnabled = settings.traffic_enabled ?? true;
|
|
35
|
+
this.trackingCacheTtlSeconds = settings.cache_ttl_seconds ?? 60;
|
|
36
|
+
this.trackingRouteCacheTtlSeconds = settings.route_cache_ttl_seconds ?? 600;
|
|
37
|
+
this.trackingStaleLocationThresholdSeconds = settings.stale_location_threshold_seconds ?? 300;
|
|
38
|
+
this.trackingDefaultVehicleSpeedKph = settings.default_vehicle_speed_kph ?? 35;
|
|
39
|
+
this.trackingProviderOptions = this.normalizeProviderOptions(settings.providers ?? this.trackingProviderOptions);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
this.notifications.serverError(error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@task *saveSettings() {
|
|
46
|
+
try {
|
|
47
|
+
yield this.fetch.post('fleet-ops/settings/admin-tracking-settings', this.trackingSettingsPayload);
|
|
48
|
+
this.notifications.success('Tracking defaults saved.');
|
|
49
|
+
} catch (error) {
|
|
50
|
+
this.notifications.serverError(error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get trackingSettingsPayload() {
|
|
55
|
+
return {
|
|
56
|
+
provider: this.trackingProvider,
|
|
57
|
+
fallbacks: this.normalizeFallbacks(this.trackingFallbacks),
|
|
58
|
+
traffic_enabled: this.trackingTrafficEnabled,
|
|
59
|
+
cache_ttl_seconds: Number(this.trackingCacheTtlSeconds) || 60,
|
|
60
|
+
route_cache_ttl_seconds: Number(this.trackingRouteCacheTtlSeconds) || 600,
|
|
61
|
+
stale_location_threshold_seconds: Number(this.trackingStaleLocationThresholdSeconds) || 300,
|
|
62
|
+
default_vehicle_speed_kph: Number(this.trackingDefaultVehicleSpeedKph) || 35,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
normalizeFallbacks(fallbacks) {
|
|
67
|
+
if (Array.isArray(fallbacks)) {
|
|
68
|
+
return fallbacks
|
|
69
|
+
.map((fallback) => this.optionValue(fallback))
|
|
70
|
+
.map((fallback) => String(fallback).trim())
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return String(fallbacks ?? '')
|
|
75
|
+
.split(',')
|
|
76
|
+
.map((fallback) => fallback.trim())
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get selectedTrackingFallbackOptions() {
|
|
81
|
+
const selected = new Set(this.normalizeFallbacks(this.trackingFallbacks));
|
|
82
|
+
|
|
83
|
+
return this.trackingProviderOptions.filter((option) => selected.has(this.optionValue(option)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
normalizeProviderOptions(options = []) {
|
|
87
|
+
return options.map((option) => {
|
|
88
|
+
const value = this.optionValue(option);
|
|
89
|
+
const label = option?.label ?? option?.name ?? this.providerLabel(value);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
...option,
|
|
93
|
+
key: option?.key ?? value,
|
|
94
|
+
name: option?.name ?? label,
|
|
95
|
+
value,
|
|
96
|
+
label,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
providerLabel(value) {
|
|
102
|
+
if (value === 'osrm') {
|
|
103
|
+
return 'OSRM';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return String(value ?? '')
|
|
107
|
+
.replace(/[_-]+/g, ' ')
|
|
108
|
+
.replace(/\s+/g, ' ')
|
|
109
|
+
.trim()
|
|
110
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
optionValue(option) {
|
|
114
|
+
return typeof option === 'object' && option !== null ? (option.value ?? option.key) : option;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@action setTrackingFallbacks(options) {
|
|
118
|
+
this.trackingFallbacks = this.normalizeFallbacks(options);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@action toggleTrackingAdvancedSettings() {
|
|
122
|
+
this.showTrackingAdvancedSettings = !this.showTrackingAdvancedSettings;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -138,7 +138,7 @@ export default class CustomerOrdersComponent extends Component {
|
|
|
138
138
|
// start loading order tracking activity
|
|
139
139
|
order.loadTrackingActivity();
|
|
140
140
|
this.urlSearchParams.addParamToCurrentUrl('order', order.public_id);
|
|
141
|
-
const driverCurrentLocation = order.get('tracker_data.
|
|
141
|
+
const driverCurrentLocation = order.get('tracker_data.driver.location');
|
|
142
142
|
if (driverCurrentLocation) {
|
|
143
143
|
this.latitude = driverCurrentLocation.coordinates[1];
|
|
144
144
|
this.longitude = driverCurrentLocation.coordinates[0];
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</span>
|
|
11
11
|
</div>
|
|
12
12
|
{{else}}
|
|
13
|
-
{{#if
|
|
13
|
+
{{#if @waypointActions}}
|
|
14
14
|
<div class="flex flex-row item-center space-x-2 {{if (or this.place.status_code @eta) 'mb-2'}}">
|
|
15
15
|
{{#unless @hideBadges}}
|
|
16
16
|
{{#if this.place.status_code}}
|
|
@@ -27,10 +27,25 @@
|
|
|
27
27
|
<div class="order-listing-row-progress">
|
|
28
28
|
<OrderProgressBar
|
|
29
29
|
@order={{@order}}
|
|
30
|
-
@progress={{@order.tracker_data.
|
|
31
|
-
@firstWaypointCompleted={{@order.tracker_data.
|
|
32
|
-
@lastWaypointCompleted={{@order.tracker_data.
|
|
30
|
+
@progress={{@order.tracker_data.progress.percentage}}
|
|
31
|
+
@firstWaypointCompleted={{gt @order.tracker_data.progress.completed_stops 0}}
|
|
32
|
+
@lastWaypointCompleted={{eq @order.tracker_data.progress.percentage 100}}
|
|
33
33
|
/>
|
|
34
|
+
{{#if @order.tracker_data.insights.is_location_stale}}
|
|
35
|
+
<div class="flex mt-1">
|
|
36
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Stale GPS</Badge>
|
|
37
|
+
</div>
|
|
38
|
+
{{else if @order.tracker_data.fallback_provider}}
|
|
39
|
+
<div class="flex mt-1">
|
|
40
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Fallback ETA</Badge>
|
|
41
|
+
</div>
|
|
42
|
+
{{else if @order.tracker_data.confidence}}
|
|
43
|
+
{{#unless (eq @order.tracker_data.confidence "high")}}
|
|
44
|
+
<div class="flex mt-1">
|
|
45
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Low ETA Confidence</Badge>
|
|
46
|
+
</div>
|
|
47
|
+
{{/unless}}
|
|
48
|
+
{{/if}}
|
|
34
49
|
</div>
|
|
35
50
|
<div class="order-listing-row-body">
|
|
36
51
|
<div class="order-listing-row-body-address start">
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
<button type="button" id="map-toolbar-view-orders-button" class="toolbar-button relative {{if unauthorized 'unauthorized'}}" {{on "click" this.orderListOverlay.toggle}}>
|
|
43
43
|
<div class="active-orders-count absolute top-0 right-0 -mr-2.5 -mt-1">
|
|
44
44
|
<div class="relative flex w-5 h-5 rounded-full">
|
|
45
|
-
<div class="absolute inset-0 rounded-full w-5 h-5 text-xs bg-blue-500 opacity-75 {{if this.
|
|
46
|
-
<div class="relative inline-flex items-center justify-center rounded-full w-5 h-5 text-xs bg-blue-500 text-white z-10">{{this.
|
|
45
|
+
<div class="absolute inset-0 rounded-full w-5 h-5 text-xs bg-blue-500 opacity-75 {{if this.displayedActiveOrderCount 'animate-ping'}}"></div>
|
|
46
|
+
<div class="relative inline-flex items-center justify-center rounded-full w-5 h-5 text-xs bg-blue-500 text-white z-10">{{this.displayedActiveOrderCount}}</div>
|
|
47
47
|
</div>
|
|
48
48
|
</div>
|
|
49
49
|
<FaIcon @icon="layer-group" />
|
|
@@ -38,10 +38,18 @@ export default class MapToolbarComponent extends Component {
|
|
|
38
38
|
return this.mapManager.zoomOut?.();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
get displayedActiveOrderCount() {
|
|
42
|
+
if (this.orderListOverlay.isOpen && this.orderListOverlay.loaded) {
|
|
43
|
+
return this.orderListOverlay.activeOrdersCount;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return this.activeOrderCount;
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
@task *getActiveOrderCount() {
|
|
42
50
|
try {
|
|
43
|
-
const count = yield this.fetch.get('fleet-ops/metrics', { discover: ['
|
|
44
|
-
this.activeOrderCount = count.
|
|
51
|
+
const count = yield this.fetch.get('fleet-ops/metrics', { discover: ['active_live_orders'] });
|
|
52
|
+
this.activeOrderCount = count.active_live_orders;
|
|
45
53
|
return count;
|
|
46
54
|
} catch (err) {
|
|
47
55
|
debug('Failed to get active order count: ' + err.message);
|
|
@@ -109,6 +109,27 @@
|
|
|
109
109
|
/>
|
|
110
110
|
</InputGroup>
|
|
111
111
|
|
|
112
|
+
{{#if this.shouldShowAllocationStrategy}}
|
|
113
|
+
<InputGroup @name={{t "orchestrator.allocation-strategy"}}>
|
|
114
|
+
<div class="grid grid-cols-2 gap-1">
|
|
115
|
+
{{#each this.allocationStrategyOptions as |opt|}}
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
class="flex items-center justify-center gap-1.5 px-2 py-1.5 rounded border text-xs transition-colors
|
|
119
|
+
{{if
|
|
120
|
+
(eq this.draftPhase.allocationStrategy opt.value)
|
|
121
|
+
'bg-indigo-50 dark:bg-indigo-900/30 border-indigo-400 dark:border-indigo-600 text-indigo-700 dark:text-indigo-300'
|
|
122
|
+
'border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-gray-300 dark:hover:border-gray-500'
|
|
123
|
+
}}"
|
|
124
|
+
{{on "click" (fn this.setDraftOption "allocationStrategy" opt.value)}}
|
|
125
|
+
>
|
|
126
|
+
{{opt.label}}
|
|
127
|
+
</button>
|
|
128
|
+
{{/each}}
|
|
129
|
+
</div>
|
|
130
|
+
</InputGroup>
|
|
131
|
+
{{/if}}
|
|
132
|
+
|
|
112
133
|
{{! Constraint toggles }}
|
|
113
134
|
<InputGroup @name={{t "orchestrator.constraints"}}>
|
|
114
135
|
<div class="space-y-1.5">
|
|
@@ -124,10 +145,12 @@
|
|
|
124
145
|
<span class="text-xs text-gray-600 dark:text-gray-400">{{t "orchestrator.balance-workload"}}</span>
|
|
125
146
|
<Toggle @isToggled={{this.draftPhase.balanceWorkload}} @onToggle={{fn this.setDraftOption "balanceWorkload"}} />
|
|
126
147
|
</div>
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
148
|
+
{{#if this.shouldShowRouteOptions}}
|
|
149
|
+
<div class="flex items-center justify-between">
|
|
150
|
+
<span class="text-xs text-gray-600 dark:text-gray-400">{{t "orchestrator.return-to-depot"}}</span>
|
|
151
|
+
<Toggle @isToggled={{this.draftPhase.returnToDepot}} @onToggle={{fn this.setDraftOption "returnToDepot"}} />
|
|
152
|
+
</div>
|
|
153
|
+
{{/if}}
|
|
131
154
|
<div class="flex items-center justify-between">
|
|
132
155
|
<span class="text-xs text-gray-600 dark:text-gray-400">{{t "orchestrator.auto-commit"}}</span>
|
|
133
156
|
<Toggle @isToggled={{this.draftPhase.autoCommit}} @onToggle={{fn this.setDraftOption "autoCommit"}} />
|
|
@@ -159,4 +182,4 @@
|
|
|
159
182
|
</div>
|
|
160
183
|
{{/if}}
|
|
161
184
|
|
|
162
|
-
</div>
|
|
185
|
+
</div>
|
|
@@ -50,6 +50,7 @@ export default class OrchestratorPhaseBuilderComponent extends Component {
|
|
|
50
50
|
mode,
|
|
51
51
|
label: this.intl.t(`orchestrator.mode-${mode.replace(/_/g, '-')}`),
|
|
52
52
|
engine: 'greedy',
|
|
53
|
+
allocationStrategy: 'route_aware',
|
|
53
54
|
orderStatuses: ['created'],
|
|
54
55
|
balanceWorkload: false,
|
|
55
56
|
respectSkills: true,
|
|
@@ -125,7 +126,11 @@ export default class OrchestratorPhaseBuilderComponent extends Component {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
@action setDraftEngine(engine) {
|
|
128
|
-
this.draftPhase = {
|
|
129
|
+
this.draftPhase = {
|
|
130
|
+
...this.draftPhase,
|
|
131
|
+
engine,
|
|
132
|
+
allocationStrategy: engine === 'capacity' ? 'capacity_only' : (this.draftPhase.allocationStrategy ?? 'route_aware'),
|
|
133
|
+
};
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
@action toggleDraftOrderStatus(status) {
|
|
@@ -146,6 +151,21 @@ export default class OrchestratorPhaseBuilderComponent extends Component {
|
|
|
146
151
|
return this.editingIndex !== null && this.draftPhase !== null;
|
|
147
152
|
}
|
|
148
153
|
|
|
154
|
+
get allocationStrategyOptions() {
|
|
155
|
+
return [
|
|
156
|
+
{ value: 'route_aware', label: this.intl.t('orchestrator.allocation-strategy-route-aware') },
|
|
157
|
+
{ value: 'capacity_only', label: this.intl.t('orchestrator.allocation-strategy-capacity-only') },
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get shouldShowAllocationStrategy() {
|
|
162
|
+
return this.draftPhase?.mode === 'assign_vehicles' && this.draftPhase?.engine === 'vroom';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get shouldShowRouteOptions() {
|
|
166
|
+
return this.draftPhase?.engine !== 'capacity' && this.draftPhase?.allocationStrategy !== 'capacity_only';
|
|
167
|
+
}
|
|
168
|
+
|
|
149
169
|
modeLabel(mode) {
|
|
150
170
|
return this.modeOptions.find((m) => m.value === mode)?.label ?? mode;
|
|
151
171
|
}
|
|
@@ -187,6 +187,15 @@
|
|
|
187
187
|
<Badge @status={{vehicle.status}}>{{smart-humanize vehicle.status}}</Badge>
|
|
188
188
|
{{/if}}
|
|
189
189
|
</div>
|
|
190
|
+
{{! Position }}
|
|
191
|
+
<div
|
|
192
|
+
class="flex items-center gap-1 text-xs truncate
|
|
193
|
+
{{if (this.hasPosition vehicle) 'text-green-500 dark:text-green-300' 'text-amber-600 dark:text-amber-300'}}"
|
|
194
|
+
title={{if (this.hasPosition vehicle) (point-coordinates vehicle.location) (t "orchestrator.no-position")}}
|
|
195
|
+
>
|
|
196
|
+
<FaIcon @icon="location-dot" @size="xs" class="w-3 flex-shrink-0" />
|
|
197
|
+
<span class="truncate">{{#if (this.hasPosition vehicle)}}{{point-coordinates vehicle.location}}{{else}}{{t "orchestrator.no-position"}}{{/if}}</span>
|
|
198
|
+
</div>
|
|
190
199
|
{{! Assigned driver }}
|
|
191
200
|
<div class="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400">
|
|
192
201
|
<FaIcon @icon="id-card-alt" @size="xs" class="w-3 flex-shrink-0" />
|
|
@@ -245,6 +254,15 @@
|
|
|
245
254
|
{{! All fields — always shown, "-" when absent }}
|
|
246
255
|
<div class="flex-1 min-w-0 space-y-0.5">
|
|
247
256
|
<div class="text-xs font-semibold truncate dark:text-white">{{or driver.name "-"}}</div>
|
|
257
|
+
{{! Position }}
|
|
258
|
+
<div
|
|
259
|
+
class="flex items-center gap-1 text-xs truncate
|
|
260
|
+
{{if (this.hasPosition driver) 'text-green-500 dark:text-green-300' 'text-amber-600 dark:text-amber-300'}}"
|
|
261
|
+
title={{if (this.hasPosition driver) (point-coordinates driver.location) (t "orchestrator.no-position")}}
|
|
262
|
+
>
|
|
263
|
+
<FaIcon @icon="location-dot" @size="xs" class="w-3 flex-shrink-0" />
|
|
264
|
+
<span class="truncate">{{#if (this.hasPosition driver)}}{{point-coordinates driver.location}}{{else}}{{t "orchestrator.no-position"}}{{/if}}</span>
|
|
265
|
+
</div>
|
|
248
266
|
{{! Vehicle }}
|
|
249
267
|
<div class="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400">
|
|
250
268
|
<FaIcon @icon="truck" @size="xs" class="w-3 flex-shrink-0" />
|
|
@@ -298,4 +316,4 @@
|
|
|
298
316
|
</div>
|
|
299
317
|
{{/if}}
|
|
300
318
|
|
|
301
|
-
</div>
|
|
319
|
+
</div>
|
|
@@ -99,6 +99,10 @@ export default class OrchestratorResourcePanelComponent extends Component {
|
|
|
99
99
|
return [...(this.args.selectedDriverIds ?? new Set())];
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
@action hasPosition(resource) {
|
|
103
|
+
return resource?.hasInvalidCoordinates === false;
|
|
104
|
+
}
|
|
105
|
+
|
|
102
106
|
@action clearAllSelections() {
|
|
103
107
|
this.args.onClearVehicles?.();
|
|
104
108
|
this.args.onClearDrivers?.();
|
|
@@ -68,7 +68,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* User-composed list of phases to execute in sequence.
|
|
71
|
-
* Each phase: { id, mode, label, engine, orderStatuses, balanceWorkload,
|
|
71
|
+
* Each phase: { id, mode, label, engine, allocationStrategy, orderStatuses, balanceWorkload,
|
|
72
72
|
* respectSkills, respectCapacity, returnToDepot, autoCommit }
|
|
73
73
|
*/
|
|
74
74
|
@tracked phases = [];
|
|
@@ -270,6 +270,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
270
270
|
order_statuses: phase.orderStatuses ?? ['created'],
|
|
271
271
|
options: {
|
|
272
272
|
engine: phase.engine ?? 'greedy',
|
|
273
|
+
allocation_strategy: phase.allocationStrategy ?? 'route_aware',
|
|
273
274
|
balance_workload: phase.balanceWorkload ?? false,
|
|
274
275
|
respect_skills: phase.respectSkills ?? true,
|
|
275
276
|
respect_capacity: phase.respectCapacity ?? true,
|
|
@@ -399,6 +400,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
399
400
|
mode: 'allocate',
|
|
400
401
|
label: 'Allocate',
|
|
401
402
|
engine: 'greedy',
|
|
403
|
+
allocationStrategy: 'route_aware',
|
|
402
404
|
orderStatuses: ['created'],
|
|
403
405
|
balanceWorkload: false,
|
|
404
406
|
respectSkills: true,
|
|
@@ -1,38 +1,35 @@
|
|
|
1
1
|
{{#if @resource.purchase_rate}}
|
|
2
|
-
<ContentPanel @title={{t "order.fields.purchase-rate-panel-title"}} @isLoading={{@isLoading}} @open={{true}} @wrapperClass="bordered-top"
|
|
3
|
-
<div class="
|
|
4
|
-
<table
|
|
2
|
+
<ContentPanel @title={{t "order.fields.purchase-rate-panel-title"}} @isLoading={{@isLoading}} @open={{true}} @wrapperClass="bordered-top">
|
|
3
|
+
<div class="overflow-x-auto">
|
|
4
|
+
<table class="w-full text-xs">
|
|
5
5
|
<thead>
|
|
6
|
-
<tr>
|
|
7
|
-
<th class="text-left">{{t "order.fields.breakdown"}}</th>
|
|
8
|
-
<th>
|
|
9
|
-
<div class="flex flex-row items-center justify-end">{{@resource.purchase_rate.service_quote.currency}}</div>
|
|
10
|
-
</th>
|
|
6
|
+
<tr class="border-b border-gray-200 dark:border-gray-700 text-[11px] uppercase tracking-wide text-gray-500 font-semibold">
|
|
7
|
+
<th class="py-2 pr-2 text-left">{{t "order.fields.breakdown"}}</th>
|
|
8
|
+
<th class="py-2 pl-2 text-right w-32">{{@resource.purchase_rate.service_quote.currency}}</th>
|
|
11
9
|
</tr>
|
|
12
10
|
</thead>
|
|
13
11
|
<tbody>
|
|
14
12
|
{{#each @resource.purchase_rate.service_quote.items as |item|}}
|
|
15
|
-
<tr>
|
|
16
|
-
<td>
|
|
17
|
-
|
|
13
|
+
<tr class="border-b border-gray-100 dark:border-gray-800 last:border-0">
|
|
14
|
+
<td class="py-1 pr-2 text-gray-700 dark:text-gray-300">
|
|
15
|
+
{{smart-humanize item.details}}
|
|
18
16
|
</td>
|
|
19
|
-
<td>
|
|
20
|
-
|
|
17
|
+
<td class="py-1 pl-2 text-right font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
|
18
|
+
{{format-currency item.amount @resource.purchase_rate.service_quote.currency}}
|
|
21
19
|
</td>
|
|
22
20
|
</tr>
|
|
23
21
|
{{/each}}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
</tbody>
|
|
23
|
+
<tfoot class="border-t border-gray-200 dark:border-gray-700">
|
|
24
|
+
<tr class="text-xs">
|
|
25
|
+
<td class="pt-2 pb-1 pr-2 text-right text-[11px] uppercase tracking-wide font-bold text-gray-800 dark:text-gray-100">
|
|
26
|
+
{{t "common.total"}}
|
|
27
27
|
</td>
|
|
28
|
-
<td class="
|
|
29
|
-
|
|
30
|
-
@resource.purchase_rate.service_quote.amount
|
|
31
|
-
@resource.purchase_rate.service_quote.currency
|
|
32
|
-
}}</div>
|
|
28
|
+
<td class="pt-2 pb-1 pl-2 text-right font-bold text-gray-800 dark:text-gray-100 whitespace-nowrap">
|
|
29
|
+
{{format-currency @resource.purchase_rate.service_quote.amount @resource.purchase_rate.service_quote.currency}}
|
|
33
30
|
</td>
|
|
34
31
|
</tr>
|
|
35
|
-
</
|
|
32
|
+
</tfoot>
|
|
36
33
|
</table>
|
|
37
34
|
</div>
|
|
38
35
|
</ContentPanel>
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
<ContentPanel @title={{t "order.fields.route-panel-title"}} @open={{true}} @actionButtons={{this.actionButtons}} @wrapperClass="bordered-top">
|
|
2
|
+
{{#if this.hasRouteSummaryLine}}
|
|
3
|
+
<div class="mb-3 flex items-center justify-between gap-3 text-xs">
|
|
4
|
+
<div class="font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">All Stops - {{this.routeStopsCount}}</div>
|
|
5
|
+
<div class="flex min-w-0 items-center gap-2 font-semibold text-gray-700 dark:text-gray-200">
|
|
6
|
+
<span>
|
|
7
|
+
{{#if this.hasTrackingDistance}}
|
|
8
|
+
{{format-meters this.trackerData.route.distance_m}}
|
|
9
|
+
{{else}}
|
|
10
|
+
-
|
|
11
|
+
{{/if}}
|
|
12
|
+
</span>
|
|
13
|
+
<span class="text-gray-300 dark:text-gray-600">-</span>
|
|
14
|
+
<span>
|
|
15
|
+
{{#if this.trackingDurationSeconds}}
|
|
16
|
+
{{format-duration this.trackingDurationSeconds}}
|
|
17
|
+
{{else if this.hasCompletionEta}}
|
|
18
|
+
{{format-date-fns this.trackerData.eta.completion_at "d MMM HH:mm"}}
|
|
19
|
+
{{else}}
|
|
20
|
+
-
|
|
21
|
+
{{/if}}
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
{{/if}}
|
|
26
|
+
|
|
2
27
|
{{#if @resource.hasIntermediateWaypoints}}
|
|
3
28
|
<div>
|
|
4
29
|
<RouteList @order={{@resource}} @eta={{@resource.eta}} @waypointActions={{hash viewWaypointLabel=(hash label=(t "order.fields.get-label") fn=this.viewWaypointLabel)}} />
|
|
@@ -19,4 +44,4 @@
|
|
|
19
44
|
</div>
|
|
20
45
|
</div>
|
|
21
46
|
{{/if}}
|
|
22
|
-
</ContentPanel>
|
|
47
|
+
</ContentPanel>
|
|
@@ -4,6 +4,7 @@ import { action } from '@ember/object';
|
|
|
4
4
|
import { debug } from '@ember/debug';
|
|
5
5
|
import { task } from 'ember-concurrency';
|
|
6
6
|
import { applyOptimizedIntermediateWaypoints, buildRouteOptimizationInput, canOptimizeIntermediateWaypoints } from '../../../utils/order-route-editing';
|
|
7
|
+
import { buildRoutePointsFromPayload } from '../../../utils/route-visualization';
|
|
7
8
|
|
|
8
9
|
export default class OrderDetailsRouteComponent extends Component {
|
|
9
10
|
@service orderActions;
|
|
@@ -15,6 +16,11 @@ export default class OrderDetailsRouteComponent extends Component {
|
|
|
15
16
|
@service hostRouter;
|
|
16
17
|
@service intl;
|
|
17
18
|
|
|
19
|
+
constructor() {
|
|
20
|
+
super(...arguments);
|
|
21
|
+
this.loadTrackerData.perform();
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
get actionButtons() {
|
|
19
25
|
return [
|
|
20
26
|
this.showOptimizationSelect
|
|
@@ -56,6 +62,50 @@ export default class OrderDetailsRouteComponent extends Component {
|
|
|
56
62
|
return this.routeOptimization.availableEngines.length > 1;
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
get trackerData() {
|
|
66
|
+
return this.args.resource?.tracker_data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get hasTrackingRouteSummary() {
|
|
70
|
+
return Boolean(this.trackerData?.route || this.trackerData?.eta);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get hasTrackingDistance() {
|
|
74
|
+
return this.trackerData?.route?.distance_m !== null && this.trackerData?.route?.distance_m !== undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get hasCompletionEta() {
|
|
78
|
+
return Boolean(this.trackerData?.eta?.completion_at);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get routeStopsCount() {
|
|
82
|
+
return buildRoutePointsFromPayload(this.args.resource?.payload).length;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get hasTrackingDuration() {
|
|
86
|
+
return this.trackerData?.route?.duration_in_traffic_s !== null && this.trackerData?.route?.duration_in_traffic_s !== undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get trackingDurationSeconds() {
|
|
90
|
+
return this.trackerData?.route?.duration_in_traffic_s ?? this.trackerData?.route?.duration_s;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get hasRouteSummaryLine() {
|
|
94
|
+
return this.hasTrackingRouteSummary || this.routeStopsCount > 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@task *loadTrackerData() {
|
|
98
|
+
if (!this.args.resource || this.args.resource.tracker_data || typeof this.args.resource.loadTrackerData !== 'function') {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
yield this.args.resource.loadTrackerData();
|
|
104
|
+
} catch (err) {
|
|
105
|
+
debug('Failed to load order tracker data for route: ' + err.message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
59
109
|
@task *optimizeRouteWithService(service) {
|
|
60
110
|
const order = this.args.resource;
|
|
61
111
|
const optimizationInput = buildRouteOptimizationInput(order);
|