@fleetbase/fleetops-engine 0.6.47 → 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/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/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 +1 -1
- 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 +40 -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
|
@@ -15,10 +15,25 @@
|
|
|
15
15
|
<div class="order-listing-row-progress mb-2">
|
|
16
16
|
<OrderProgressBar
|
|
17
17
|
@card={{@card}}
|
|
18
|
-
@progress={{@card.tracker_data.
|
|
19
|
-
@firstWaypointCompleted={{@card.tracker_data.
|
|
20
|
-
@lastWaypointCompleted={{@card.tracker_data.
|
|
18
|
+
@progress={{@card.tracker_data.progress.percentage}}
|
|
19
|
+
@firstWaypointCompleted={{gt @card.tracker_data.progress.completed_stops 0}}
|
|
20
|
+
@lastWaypointCompleted={{eq @card.tracker_data.progress.percentage 100}}
|
|
21
21
|
/>
|
|
22
|
+
{{#if @card.tracker_data.insights.is_location_stale}}
|
|
23
|
+
<div class="flex mt-1">
|
|
24
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Stale GPS</Badge>
|
|
25
|
+
</div>
|
|
26
|
+
{{else if @card.tracker_data.fallback_provider}}
|
|
27
|
+
<div class="flex mt-1">
|
|
28
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Fallback ETA</Badge>
|
|
29
|
+
</div>
|
|
30
|
+
{{else if @card.tracker_data.confidence}}
|
|
31
|
+
{{#unless (eq @card.tracker_data.confidence "high")}}
|
|
32
|
+
<div class="flex mt-1">
|
|
33
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Low ETA Confidence</Badge>
|
|
34
|
+
</div>
|
|
35
|
+
{{/unless}}
|
|
36
|
+
{{/if}}
|
|
22
37
|
</div>
|
|
23
38
|
<div class="order-listing-row-body flex flex-row">
|
|
24
39
|
<div class="flex-1 order-listing-row-body-address start">
|
|
@@ -26,10 +26,25 @@
|
|
|
26
26
|
<div class="order-listing-row-progress">
|
|
27
27
|
<OrderProgressBar
|
|
28
28
|
@order={{@order}}
|
|
29
|
-
@progress={{@order.tracker_data.
|
|
30
|
-
@firstWaypointCompleted={{@order.tracker_data.
|
|
31
|
-
@lastWaypointCompleted={{@order.tracker_data.
|
|
29
|
+
@progress={{@order.tracker_data.progress.percentage}}
|
|
30
|
+
@firstWaypointCompleted={{gt @order.tracker_data.progress.completed_stops 0}}
|
|
31
|
+
@lastWaypointCompleted={{eq @order.tracker_data.progress.percentage 100}}
|
|
32
32
|
/>
|
|
33
|
+
{{#if @order.tracker_data.insights.is_location_stale}}
|
|
34
|
+
<div class="flex mt-1">
|
|
35
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Stale GPS</Badge>
|
|
36
|
+
</div>
|
|
37
|
+
{{else if @order.tracker_data.fallback_provider}}
|
|
38
|
+
<div class="flex mt-1">
|
|
39
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Fallback ETA</Badge>
|
|
40
|
+
</div>
|
|
41
|
+
{{else if @order.tracker_data.confidence}}
|
|
42
|
+
{{#unless (eq @order.tracker_data.confidence "high")}}
|
|
43
|
+
<div class="flex mt-1">
|
|
44
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Low ETA Confidence</Badge>
|
|
45
|
+
</div>
|
|
46
|
+
{{/unless}}
|
|
47
|
+
{{/if}}
|
|
33
48
|
</div>
|
|
34
49
|
<div class="order-listing-row-body">
|
|
35
50
|
<div class="order-listing-row-body-address start">
|
|
@@ -16,11 +16,26 @@
|
|
|
16
16
|
<div class="order-progress-card-progress">
|
|
17
17
|
<OrderProgressBar
|
|
18
18
|
@order={{this.order}}
|
|
19
|
-
@progress={{this.order.tracker_data.
|
|
20
|
-
@firstWaypointCompleted={{this.order.tracker_data.
|
|
21
|
-
@lastWaypointCompleted={{this.order.tracker_data.
|
|
19
|
+
@progress={{this.order.tracker_data.progress.percentage}}
|
|
20
|
+
@firstWaypointCompleted={{gt this.order.tracker_data.progress.completed_stops 0}}
|
|
21
|
+
@lastWaypointCompleted={{eq this.order.tracker_data.progress.percentage 100}}
|
|
22
22
|
/>
|
|
23
23
|
</div>
|
|
24
|
+
{{#if this.order.tracker_data.insights.is_location_stale}}
|
|
25
|
+
<div class="flex px-1 mt-1">
|
|
26
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Stale GPS</Badge>
|
|
27
|
+
</div>
|
|
28
|
+
{{else if this.order.tracker_data.fallback_provider}}
|
|
29
|
+
<div class="flex px-1 mt-1">
|
|
30
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Fallback ETA</Badge>
|
|
31
|
+
</div>
|
|
32
|
+
{{else if this.order.tracker_data.confidence}}
|
|
33
|
+
{{#unless (eq this.order.tracker_data.confidence "high")}}
|
|
34
|
+
<div class="flex px-1 mt-1">
|
|
35
|
+
<Badge @status="warning" @hideStatusDot={{true}}>Low ETA Confidence</Badge>
|
|
36
|
+
</div>
|
|
37
|
+
{{/unless}}
|
|
38
|
+
{{/if}}
|
|
24
39
|
<div class="order-progress-card-details">
|
|
25
40
|
<div class="order-progress-card-details-address start">
|
|
26
41
|
{{#if this.order.payload.isMultiDrop}}
|
|
@@ -41,11 +56,11 @@
|
|
|
41
56
|
<div class="grid grid-cols-2 gap-2 mt-1 px-1">
|
|
42
57
|
<div class="flex flex-col text-xs">
|
|
43
58
|
<div class="font-semibold">{{t "order.fields.current-eta"}}:</div>
|
|
44
|
-
<div class="">{{format-duration this.order.tracker_data.
|
|
59
|
+
<div class="">{{format-duration this.order.tracker_data.eta.active_stop_seconds}}</div>
|
|
45
60
|
</div>
|
|
46
61
|
<div class="flex flex-col text-xs">
|
|
47
62
|
<div class="font-semibold">{{t "order.fields.ect"}}:</div>
|
|
48
|
-
<div class="">{{n-a this.order.tracker_data.
|
|
63
|
+
<div class="">{{n-a this.order.tracker_data.eta.completion_at}}</div>
|
|
49
64
|
</div>
|
|
50
65
|
<div class="flex flex-col text-xs">
|
|
51
66
|
<div class="font-semibold">{{t "order.fields.driver"}}:</div>
|
|
@@ -57,7 +72,7 @@
|
|
|
57
72
|
</div>
|
|
58
73
|
<div class="flex flex-col text-xs">
|
|
59
74
|
<div class="font-semibold">{{t "order.fields.current-destination"}}:</div>
|
|
60
|
-
<div class="truncate">{{n-a this.order.tracker_data.
|
|
75
|
+
<div class="truncate">{{n-a this.order.tracker_data.active_stop.address}}</div>
|
|
61
76
|
</div>
|
|
62
77
|
</div>
|
|
63
78
|
</div>
|
|
@@ -90,27 +90,27 @@
|
|
|
90
90
|
<div class="order-progress-card-progress">
|
|
91
91
|
<OrderProgressBar
|
|
92
92
|
@order={{this.order}}
|
|
93
|
-
@progress={{this.order.tracker_data.
|
|
94
|
-
@firstWaypointCompleted={{this.order.tracker_data.
|
|
95
|
-
@lastWaypointCompleted={{this.order.tracker_data.
|
|
93
|
+
@progress={{this.order.tracker_data.progress.percentage}}
|
|
94
|
+
@firstWaypointCompleted={{gt this.order.tracker_data.progress.completed_stops 0}}
|
|
95
|
+
@lastWaypointCompleted={{eq this.order.tracker_data.progress.percentage 100}}
|
|
96
96
|
/>
|
|
97
97
|
</div>
|
|
98
98
|
<div class="grid grid-cols-2 gap-2 mt-1 px-1">
|
|
99
99
|
<div class="flex flex-col text-xs">
|
|
100
100
|
<div class="font-semibold">Current ETA:</div>
|
|
101
|
-
<div class="">{{format-duration this.order.tracker_data.
|
|
101
|
+
<div class="">{{format-duration this.order.tracker_data.eta.active_stop_seconds}}</div>
|
|
102
102
|
</div>
|
|
103
103
|
<div class="flex flex-col text-xs">
|
|
104
104
|
<div class="font-semibold">ECT:</div>
|
|
105
|
-
<div class="">{{n-a this.order.tracker_data.
|
|
105
|
+
<div class="">{{n-a this.order.tracker_data.eta.completion_at}}</div>
|
|
106
106
|
</div>
|
|
107
107
|
<div class="col-span-2 flex flex-col text-xs">
|
|
108
108
|
<div class="font-semibold">Current Destination:</div>
|
|
109
|
-
<div class="truncate">{{n-a this.order.tracker_data.
|
|
109
|
+
<div class="truncate">{{n-a this.order.tracker_data.active_stop.address}}</div>
|
|
110
110
|
</div>
|
|
111
111
|
<div class="col-span-2 flex flex-col text-xs">
|
|
112
112
|
<div class="font-semibold">Next Destination:</div>
|
|
113
|
-
<div class="truncate">{{n-a this.order.tracker_data.
|
|
113
|
+
<div class="truncate">{{n-a this.order.tracker_data.next_stop.address}}</div>
|
|
114
114
|
</div>
|
|
115
115
|
</div>
|
|
116
116
|
</div>
|
|
@@ -50,7 +50,7 @@ export default class OrderTrackingLookupComponent extends Component {
|
|
|
50
50
|
try {
|
|
51
51
|
this.order = yield this.fetch.get('fleet-ops/lookup', { tracking: this.trackingNumber }, { normalizeToEmberData: true, normalizeModelType: 'order' });
|
|
52
52
|
this.urlSearchParams.addParamToCurrentUrl('order', this.order.tracking);
|
|
53
|
-
const driverCurrentLocation = this.order.get('tracker_data.
|
|
53
|
+
const driverCurrentLocation = this.order.get('tracker_data.driver.location');
|
|
54
54
|
if (driverCurrentLocation) {
|
|
55
55
|
this.latitude = driverCurrentLocation.coordinates[1];
|
|
56
56
|
this.longitude = driverCurrentLocation.coordinates[0];
|
|
@@ -2,10 +2,29 @@
|
|
|
2
2
|
{{#if this.firstStop}}
|
|
3
3
|
<div class="order-route-stop {{@routeStopClass}}">
|
|
4
4
|
<div class="order-route-stop-index {{@routeStopIndexClass}}">
|
|
5
|
-
<div
|
|
5
|
+
<div
|
|
6
|
+
class="index-count fleetops-route-stop-badge {{if this.firstStop.active 'fleetops-route-stop-badge--active'}}"
|
|
7
|
+
style={{this.firstStop.badgeStyle}}
|
|
8
|
+
>{{this.firstStop.label}}</div>
|
|
6
9
|
</div>
|
|
7
10
|
<div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
|
|
8
|
-
<
|
|
11
|
+
<div class="mb-2 flex flex-wrap items-center gap-1.5">
|
|
12
|
+
{{#if this.firstStop.place.status_code}}
|
|
13
|
+
<Badge @status={{this.firstStop.place.status_code}} />
|
|
14
|
+
{{/if}}
|
|
15
|
+
{{#if this.firstStop.completed}}
|
|
16
|
+
<Badge @status="success" @hideStatusDot={{true}}>Completed</Badge>
|
|
17
|
+
{{else if this.firstStop.active}}
|
|
18
|
+
<Badge @status="indigo" @hideStatusDot={{true}}>Current Stop</Badge>
|
|
19
|
+
{{/if}}
|
|
20
|
+
{{#if this.firstStop.etaSeconds}}
|
|
21
|
+
<Badge @status="pending" @hideStatusDot={{true}}><span class="font-bold">ETA:</span> {{format-duration this.firstStop.etaSeconds}}</Badge>
|
|
22
|
+
{{/if}}
|
|
23
|
+
{{#if this.firstStop.etaAt}}
|
|
24
|
+
<Badge @status="pending" @hideStatusDot={{true}}>{{format-date-fns this.firstStop.etaAt "d MMM HH:mm"}}</Badge>
|
|
25
|
+
{{/if}}
|
|
26
|
+
</div>
|
|
27
|
+
<DisplayPlace @place={{this.firstStop.place}} @waypointActions={{@waypointActions}} @hideBadges={{true}} />
|
|
9
28
|
</div>
|
|
10
29
|
</div>
|
|
11
30
|
{{/if}}
|
|
@@ -24,10 +43,26 @@
|
|
|
24
43
|
{{#each this.middleStops as |stop|}}
|
|
25
44
|
<div class="order-route-stop {{@routeStopClass}}">
|
|
26
45
|
<div class="order-route-stop-index {{@routeStopIndexClass}}">
|
|
27
|
-
<div class="index-count fleetops-route-stop-badge" style={{stop.badgeStyle}}>{{stop.label}}</div>
|
|
46
|
+
<div class="index-count fleetops-route-stop-badge {{if stop.active 'fleetops-route-stop-badge--active'}}" style={{stop.badgeStyle}}>{{stop.label}}</div>
|
|
28
47
|
</div>
|
|
29
48
|
<div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
|
|
30
|
-
<
|
|
49
|
+
<div class="mb-2 flex flex-wrap items-center gap-1.5">
|
|
50
|
+
{{#if stop.place.status_code}}
|
|
51
|
+
<Badge @status={{stop.place.status_code}} />
|
|
52
|
+
{{/if}}
|
|
53
|
+
{{#if stop.completed}}
|
|
54
|
+
<Badge @status="success" @hideStatusDot={{true}}>Completed</Badge>
|
|
55
|
+
{{else if stop.active}}
|
|
56
|
+
<Badge @status="indigo" @hideStatusDot={{true}}>Current Stop</Badge>
|
|
57
|
+
{{/if}}
|
|
58
|
+
{{#if stop.etaSeconds}}
|
|
59
|
+
<Badge @status="pending" @hideStatusDot={{true}}><span class="font-bold">ETA:</span> {{format-duration stop.etaSeconds}}</Badge>
|
|
60
|
+
{{/if}}
|
|
61
|
+
{{#if stop.etaAt}}
|
|
62
|
+
<Badge @status="pending" @hideStatusDot={{true}}>{{format-date-fns stop.etaAt "d MMM HH:mm"}}</Badge>
|
|
63
|
+
{{/if}}
|
|
64
|
+
</div>
|
|
65
|
+
<DisplayPlace @place={{stop.place}} @waypointActions={{@waypointActions}} @hideBadges={{true}} />
|
|
31
66
|
</div>
|
|
32
67
|
</div>
|
|
33
68
|
{{/each}}
|
|
@@ -36,10 +71,26 @@
|
|
|
36
71
|
{{#each this.middleStops as |stop|}}
|
|
37
72
|
<div class="order-route-stop {{@routeStopClass}}">
|
|
38
73
|
<div class="order-route-stop-index {{@routeStopIndexClass}}">
|
|
39
|
-
<div class="index-count fleetops-route-stop-badge" style={{stop.badgeStyle}}>{{stop.label}}</div>
|
|
74
|
+
<div class="index-count fleetops-route-stop-badge {{if stop.active 'fleetops-route-stop-badge--active'}}" style={{stop.badgeStyle}}>{{stop.label}}</div>
|
|
40
75
|
</div>
|
|
41
76
|
<div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
|
|
42
|
-
<
|
|
77
|
+
<div class="mb-2 flex flex-wrap items-center gap-1.5">
|
|
78
|
+
{{#if stop.place.status_code}}
|
|
79
|
+
<Badge @status={{stop.place.status_code}} />
|
|
80
|
+
{{/if}}
|
|
81
|
+
{{#if stop.completed}}
|
|
82
|
+
<Badge @status="success" @hideStatusDot={{true}}>Completed</Badge>
|
|
83
|
+
{{else if stop.active}}
|
|
84
|
+
<Badge @status="indigo" @hideStatusDot={{true}}>Current Stop</Badge>
|
|
85
|
+
{{/if}}
|
|
86
|
+
{{#if stop.etaSeconds}}
|
|
87
|
+
<Badge @status="pending" @hideStatusDot={{true}}><span class="font-bold">ETA:</span> {{format-duration stop.etaSeconds}}</Badge>
|
|
88
|
+
{{/if}}
|
|
89
|
+
{{#if stop.etaAt}}
|
|
90
|
+
<Badge @status="pending" @hideStatusDot={{true}}>{{format-date-fns stop.etaAt "d MMM HH:mm"}}</Badge>
|
|
91
|
+
{{/if}}
|
|
92
|
+
</div>
|
|
93
|
+
<DisplayPlace @place={{stop.place}} @waypointActions={{@waypointActions}} @hideBadges={{true}} />
|
|
43
94
|
</div>
|
|
44
95
|
</div>
|
|
45
96
|
{{/each}}
|
|
@@ -48,10 +99,29 @@
|
|
|
48
99
|
{{#if this.lastStop}}
|
|
49
100
|
<div class="order-route-stop {{@routeStopClass}}">
|
|
50
101
|
<div class="order-route-stop-index {{@routeStopIndexClass}}">
|
|
51
|
-
<div
|
|
102
|
+
<div
|
|
103
|
+
class="index-count fleetops-route-stop-badge {{if this.lastStop.active 'fleetops-route-stop-badge--active'}}"
|
|
104
|
+
style={{this.lastStop.badgeStyle}}
|
|
105
|
+
>{{this.lastStop.label}}</div>
|
|
52
106
|
</div>
|
|
53
107
|
<div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
|
|
54
|
-
<
|
|
108
|
+
<div class="mb-2 flex flex-wrap items-center gap-1.5">
|
|
109
|
+
{{#if this.lastStop.place.status_code}}
|
|
110
|
+
<Badge @status={{this.lastStop.place.status_code}} />
|
|
111
|
+
{{/if}}
|
|
112
|
+
{{#if this.lastStop.completed}}
|
|
113
|
+
<Badge @status="success" @hideStatusDot={{true}}>Completed</Badge>
|
|
114
|
+
{{else if this.lastStop.active}}
|
|
115
|
+
<Badge @status="indigo" @hideStatusDot={{true}}>Current Stop</Badge>
|
|
116
|
+
{{/if}}
|
|
117
|
+
{{#if this.lastStop.etaSeconds}}
|
|
118
|
+
<Badge @status="pending" @hideStatusDot={{true}}><span class="font-bold">ETA:</span> {{format-duration this.lastStop.etaSeconds}}</Badge>
|
|
119
|
+
{{/if}}
|
|
120
|
+
{{#if this.lastStop.etaAt}}
|
|
121
|
+
<Badge @status="pending" @hideStatusDot={{true}}>{{format-date-fns this.lastStop.etaAt "d MMM HH:mm"}}</Badge>
|
|
122
|
+
{{/if}}
|
|
123
|
+
</div>
|
|
124
|
+
<DisplayPlace @place={{this.lastStop.place}} @waypointActions={{@waypointActions}} @hideBadges={{true}} />
|
|
55
125
|
</div>
|
|
56
126
|
</div>
|
|
57
127
|
{{/if}}
|
|
@@ -16,12 +16,22 @@ export default class RouteListComponent extends Component {
|
|
|
16
16
|
get routeStops() {
|
|
17
17
|
const routePoints = buildRoutePointsFromPayload(this.args.order?.payload);
|
|
18
18
|
|
|
19
|
-
return routePoints
|
|
20
|
-
routePoint
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
return routePoints
|
|
20
|
+
.map((routePoint) => ({
|
|
21
|
+
routePoint,
|
|
22
|
+
place: routePoint.place,
|
|
23
|
+
badgeStyle: this.badgeStyleForStop(routePoint),
|
|
24
|
+
trackingStop: this.trackingStopFor(routePoint.place),
|
|
25
|
+
routeLeg: this.routeLegFor(routePoint.place),
|
|
26
|
+
...describeRoutePoint(routePoint, this.routeColor),
|
|
27
|
+
}))
|
|
28
|
+
.map((stop) => ({
|
|
29
|
+
...stop,
|
|
30
|
+
etaSeconds: stop.routeLeg?.eta_seconds ?? stop.routeLeg?.duration_in_traffic_s ?? stop.routeLeg?.duration_s ?? this.legacyEtaFor(stop.place),
|
|
31
|
+
etaAt: stop.routeLeg?.eta_at,
|
|
32
|
+
completed: Boolean(stop.trackingStop?.completed),
|
|
33
|
+
active: this.matchesStop(stop.trackingStop, this.args.order?.tracker_data?.active_stop),
|
|
34
|
+
}));
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
get firstStop() {
|
|
@@ -52,6 +62,38 @@ export default class RouteListComponent extends Component {
|
|
|
52
62
|
return `background-color: ${markerColor}; color: ${textColor};`;
|
|
53
63
|
}
|
|
54
64
|
|
|
65
|
+
trackingStopFor(place) {
|
|
66
|
+
const stops = this.args.order?.tracker_data?.stops ?? [];
|
|
67
|
+
|
|
68
|
+
return stops.find((stop) => this.matchesPlace(stop, place)) ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
routeLegFor(place) {
|
|
72
|
+
const legs = this.args.order?.tracker_data?.route?.legs ?? [];
|
|
73
|
+
|
|
74
|
+
return legs.find((leg) => this.matchesPlace(leg.stop, place)) ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
legacyEtaFor(place) {
|
|
78
|
+
return this.args.eta?.[place?.id] ?? this.args.eta?.[place?.uuid] ?? this.args.eta?.[place?.public_id] ?? null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
matchesPlace(stop, place) {
|
|
82
|
+
if (!stop || !place) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return stop.uuid === place.uuid || stop.public_id === place.public_id || stop.id === place.id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
matchesStop(stop, activeStop) {
|
|
90
|
+
if (!stop || !activeStop) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return stop.uuid === activeStop.uuid || stop.public_id === activeStop.public_id;
|
|
95
|
+
}
|
|
96
|
+
|
|
55
97
|
@action toggleWaypointsCollapse() {
|
|
56
98
|
this.isWaypointsCollapsed = !this.isWaypointsCollapsed;
|
|
57
99
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<div class="tracking-stop-progress" ...attributes>
|
|
2
|
+
<div class="tracking-stop-progress__header">
|
|
3
|
+
<div class="tracking-stop-progress__title">Between Stops</div>
|
|
4
|
+
<div class="tracking-stop-progress__count">
|
|
5
|
+
<span class="font-semibold text-gray-800 dark:text-gray-100">{{this.completedCount}}</span>
|
|
6
|
+
/
|
|
7
|
+
{{this.totalCount}}
|
|
8
|
+
stops
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="tracking-stop-progress__scroller">
|
|
13
|
+
<div class="tracking-stop-progress__rail">
|
|
14
|
+
{{#each this.stops as |stop|}}
|
|
15
|
+
<div class="tracking-stop-progress__stop">
|
|
16
|
+
<div class="tracking-stop-progress__line">
|
|
17
|
+
{{#unless stop.isFirst}}
|
|
18
|
+
<div class="tracking-stop-progress__connector"></div>
|
|
19
|
+
{{/unless}}
|
|
20
|
+
|
|
21
|
+
<div
|
|
22
|
+
class="tracking-stop-progress__dot
|
|
23
|
+
{{if stop.completed 'tracking-stop-progress__dot--done'}}
|
|
24
|
+
{{if stop.active 'tracking-stop-progress__dot--active'}}
|
|
25
|
+
{{if stop.pending 'tracking-stop-progress__dot--pending'}}"
|
|
26
|
+
title={{stop.title}}
|
|
27
|
+
>
|
|
28
|
+
{{stop.label}}
|
|
29
|
+
</div>
|
|
30
|
+
<Attach::Tooltip @class="clean" @animation="scale" @placement="top">
|
|
31
|
+
<InputInfo class="max-w-xs">
|
|
32
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{stop.title}}</div>
|
|
33
|
+
<DisplayPlace @place={{stop.place}} @addressClass="mt-1 text-xs leading-4 text-gray-900 dark:text-gray-100 not-italic" @hideBadges={{true}} />
|
|
34
|
+
</InputInfo>
|
|
35
|
+
</Attach::Tooltip>
|
|
36
|
+
|
|
37
|
+
{{#unless stop.isLast}}
|
|
38
|
+
<div class="tracking-stop-progress__connector"></div>
|
|
39
|
+
{{/unless}}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
{{/each}}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
|
|
3
|
+
export default class TrackingStopProgressComponent extends Component {
|
|
4
|
+
get stops() {
|
|
5
|
+
const stops = this.args.stops ?? [];
|
|
6
|
+
const activeStop = this.args.activeStop;
|
|
7
|
+
|
|
8
|
+
return stops.map((stop, index) => {
|
|
9
|
+
const isActive = this.matches(stop, activeStop);
|
|
10
|
+
const completed = Boolean(stop.completed);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
...stop,
|
|
14
|
+
index: index + 1,
|
|
15
|
+
isFirst: index === 0,
|
|
16
|
+
isLast: index === stops.length - 1,
|
|
17
|
+
label: this.labelFor(stop, index),
|
|
18
|
+
title: this.titleFor(stop, index),
|
|
19
|
+
locationLabel: this.locationLabelFor(stop, index),
|
|
20
|
+
place: this.placeFor(stop),
|
|
21
|
+
completed,
|
|
22
|
+
active: isActive,
|
|
23
|
+
pending: !completed && !isActive,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get completedCount() {
|
|
29
|
+
return this.stops.filter((stop) => stop.completed).length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get totalCount() {
|
|
33
|
+
return this.stops.length;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
labelFor(stop, index) {
|
|
37
|
+
if (stop.type === 'pickup') {
|
|
38
|
+
return 'P';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (stop.type === 'dropoff') {
|
|
42
|
+
return 'D';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return String(index + 1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
titleFor(stop, index) {
|
|
49
|
+
if (stop.type === 'pickup') {
|
|
50
|
+
return 'Pickup';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (stop.type === 'dropoff') {
|
|
54
|
+
return 'Dropoff';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return `Stop ${index + 1}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
locationLabelFor(stop, index) {
|
|
61
|
+
return stop.city || stop.name || stop.address || this.titleFor(stop, index);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
placeFor(stop) {
|
|
65
|
+
return {
|
|
66
|
+
...stop,
|
|
67
|
+
street1: stop.street1 || stop.address || stop.name,
|
|
68
|
+
country_name: stop.country_name || stop.country,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
matches(stop, activeStop) {
|
|
73
|
+
if (!stop || !activeStop) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return stop.uuid === activeStop.uuid || stop.public_id === activeStop.public_id;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Controller from '@ember/controller';
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { inject as service } from '@ember/service';
|
|
4
|
+
import { action } from '@ember/object';
|
|
4
5
|
import { task } from 'ember-concurrency';
|
|
5
6
|
|
|
6
7
|
export default class SettingsRoutingController extends Controller {
|
|
@@ -12,6 +13,19 @@ export default class SettingsRoutingController extends Controller {
|
|
|
12
13
|
@tracked displayEngine = 'osrm';
|
|
13
14
|
@tracked optimizationEngine = 'osrm';
|
|
14
15
|
@tracked routingUnit = 'km';
|
|
16
|
+
@tracked trackingProvider = 'google_routes';
|
|
17
|
+
@tracked trackingFallbacks = ['osrm', 'calculated'];
|
|
18
|
+
@tracked trackingTrafficEnabled = true;
|
|
19
|
+
@tracked trackingCacheTtlSeconds = 60;
|
|
20
|
+
@tracked trackingRouteCacheTtlSeconds = 600;
|
|
21
|
+
@tracked trackingStaleLocationThresholdSeconds = 300;
|
|
22
|
+
@tracked trackingDefaultVehicleSpeedKph = 35;
|
|
23
|
+
@tracked showTrackingAdvancedSettings = false;
|
|
24
|
+
@tracked trackingProviderOptions = [
|
|
25
|
+
{ value: 'google_routes', label: 'Google Routes' },
|
|
26
|
+
{ value: 'osrm', label: 'OSRM' },
|
|
27
|
+
{ value: 'calculated', label: 'Calculated' },
|
|
28
|
+
];
|
|
15
29
|
@tracked routingUnitOptions = [
|
|
16
30
|
{ label: 'Kilometers', value: 'km' },
|
|
17
31
|
{ label: 'Miles', value: 'mi' },
|
|
@@ -39,6 +53,8 @@ export default class SettingsRoutingController extends Controller {
|
|
|
39
53
|
optimization_engine: this.optimizationEngine,
|
|
40
54
|
unit: this.routingUnit,
|
|
41
55
|
});
|
|
56
|
+
const trackingSettings = this.trackingSettingsPayload;
|
|
57
|
+
yield this.fetch.post('fleet-ops/settings/tracking-settings', trackingSettings);
|
|
42
58
|
yield this.performAdditionalSaveTasks();
|
|
43
59
|
// Save in local memory too
|
|
44
60
|
this.currentUser.setOption('routing', {
|
|
@@ -47,7 +63,8 @@ export default class SettingsRoutingController extends Controller {
|
|
|
47
63
|
routing_optimization_engine: this.optimizationEngine,
|
|
48
64
|
unit: this.routingUnit,
|
|
49
65
|
});
|
|
50
|
-
this.
|
|
66
|
+
this.currentUser.setOption('tracking', trackingSettings);
|
|
67
|
+
this.notifications.success('Routing and tracking settings saved.');
|
|
51
68
|
} catch (error) {
|
|
52
69
|
this.notifications.serverError(error);
|
|
53
70
|
}
|
|
@@ -64,11 +81,91 @@ export default class SettingsRoutingController extends Controller {
|
|
|
64
81
|
this.displayEngine = display_engine ?? router ?? 'osrm';
|
|
65
82
|
this.optimizationEngine = optimization_engine ?? display_engine ?? router ?? 'osrm';
|
|
66
83
|
this.routingUnit = unit;
|
|
84
|
+
const trackingSettings = yield this.fetch.get('fleet-ops/settings/tracking-settings');
|
|
85
|
+
this.trackingProvider = trackingSettings.provider ?? 'google_routes';
|
|
86
|
+
this.trackingFallbacks = this.normalizeFallbacks(trackingSettings.fallbacks);
|
|
87
|
+
this.trackingTrafficEnabled = trackingSettings.traffic_enabled ?? true;
|
|
88
|
+
this.trackingCacheTtlSeconds = trackingSettings.cache_ttl_seconds ?? 60;
|
|
89
|
+
this.trackingRouteCacheTtlSeconds = trackingSettings.route_cache_ttl_seconds ?? 600;
|
|
90
|
+
this.trackingStaleLocationThresholdSeconds = trackingSettings.stale_location_threshold_seconds ?? 300;
|
|
91
|
+
this.trackingDefaultVehicleSpeedKph = trackingSettings.default_vehicle_speed_kph ?? 35;
|
|
92
|
+
this.trackingProviderOptions = this.normalizeProviderOptions(trackingSettings.providers ?? this.trackingProviderOptions);
|
|
67
93
|
} catch (error) {
|
|
68
94
|
this.notifications.serverError(error);
|
|
69
95
|
}
|
|
70
96
|
}
|
|
71
97
|
|
|
98
|
+
get selectedTrackingFallbackOptions() {
|
|
99
|
+
const selected = new Set(this.normalizeFallbacks(this.trackingFallbacks));
|
|
100
|
+
|
|
101
|
+
return this.trackingProviderOptions.filter((option) => selected.has(this.optionValue(option)));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get trackingSettingsPayload() {
|
|
105
|
+
return {
|
|
106
|
+
provider: this.trackingProvider,
|
|
107
|
+
fallbacks: this.normalizeFallbacks(this.trackingFallbacks),
|
|
108
|
+
traffic_enabled: this.trackingTrafficEnabled,
|
|
109
|
+
cache_ttl_seconds: Number(this.trackingCacheTtlSeconds) || 60,
|
|
110
|
+
route_cache_ttl_seconds: Number(this.trackingRouteCacheTtlSeconds) || 600,
|
|
111
|
+
stale_location_threshold_seconds: Number(this.trackingStaleLocationThresholdSeconds) || 300,
|
|
112
|
+
default_vehicle_speed_kph: Number(this.trackingDefaultVehicleSpeedKph) || 35,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
normalizeFallbacks(fallbacks) {
|
|
117
|
+
if (Array.isArray(fallbacks)) {
|
|
118
|
+
return fallbacks
|
|
119
|
+
.map((fallback) => this.optionValue(fallback))
|
|
120
|
+
.map((fallback) => String(fallback).trim())
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return String(fallbacks ?? '')
|
|
125
|
+
.split(',')
|
|
126
|
+
.map((fallback) => fallback.trim())
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
normalizeProviderOptions(options = []) {
|
|
131
|
+
return options.map((option) => {
|
|
132
|
+
const value = this.optionValue(option);
|
|
133
|
+
const label = option?.label ?? option?.name ?? this.providerLabel(value);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
...option,
|
|
137
|
+
key: option?.key ?? value,
|
|
138
|
+
name: option?.name ?? label,
|
|
139
|
+
value,
|
|
140
|
+
label,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
providerLabel(value) {
|
|
146
|
+
if (value === 'osrm') {
|
|
147
|
+
return 'OSRM';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return String(value ?? '')
|
|
151
|
+
.replace(/[_-]+/g, ' ')
|
|
152
|
+
.replace(/\s+/g, ' ')
|
|
153
|
+
.trim()
|
|
154
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
optionValue(option) {
|
|
158
|
+
return typeof option === 'object' && option !== null ? (option.value ?? option.key) : option;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@action setTrackingFallbacks(options) {
|
|
162
|
+
this.trackingFallbacks = this.normalizeFallbacks(options);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@action toggleTrackingAdvancedSettings() {
|
|
166
|
+
this.showTrackingAdvancedSettings = !this.showTrackingAdvancedSettings;
|
|
167
|
+
}
|
|
168
|
+
|
|
72
169
|
registerSaveTask(key, task) {
|
|
73
170
|
if (!key || typeof task?.perform !== 'function') {
|
|
74
171
|
return;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { helper } from '@ember/component/helper';
|
|
2
|
+
|
|
3
|
+
export function formatDurationValue(seconds) {
|
|
4
|
+
const value = Number(seconds);
|
|
5
|
+
|
|
6
|
+
if (!Number.isFinite(value)) {
|
|
7
|
+
return '0s';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const totalSeconds = Math.max(0, Math.ceil(value));
|
|
11
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
12
|
+
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
13
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
14
|
+
const remainingSeconds = totalSeconds % 60;
|
|
15
|
+
const parts = [];
|
|
16
|
+
|
|
17
|
+
if (days) {
|
|
18
|
+
parts.push(`${days}d`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (hours) {
|
|
22
|
+
parts.push(`${hours}h`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!days && minutes) {
|
|
26
|
+
parts.push(`${minutes}m`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!days && !hours && parts.length < 2 && remainingSeconds) {
|
|
30
|
+
parts.push(`${remainingSeconds}s`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return parts.length ? parts.join(' ') : '0s';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default helper(function formatDuration([seconds]) {
|
|
37
|
+
return formatDurationValue(seconds);
|
|
38
|
+
});
|