@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
|
@@ -1,40 +1,184 @@
|
|
|
1
1
|
<ContentPanel @title={{t "order.fields.tracking"}} @prefixTitleRight={{@resource.tracking}} @isLoading={{@isLoading}} @open={{true}} @wrapperClass="bordered-top">
|
|
2
|
-
{{#if
|
|
3
|
-
<div class="mb-4">
|
|
4
|
-
<div class="
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
2
|
+
{{#if this.hasTrackerData}}
|
|
3
|
+
<div class="tracking-intelligence mb-4">
|
|
4
|
+
<div class="tracking-intelligence__status">
|
|
5
|
+
<span class="tracking-intelligence-pill {{this.confidenceToneClass}}">
|
|
6
|
+
<span class="tracking-intelligence-pill__dot"></span>
|
|
7
|
+
{{this.confidenceLabel}}
|
|
8
|
+
confidence ·
|
|
9
|
+
{{this.confidencePercent}}%
|
|
10
|
+
</span>
|
|
11
|
+
<span class="tracking-intelligence-pill {{if (eq this.driverSignal 'Live') 'tracking-intelligence-pill--good' 'tracking-intelligence-pill--warn'}}">
|
|
12
|
+
<span class="tracking-intelligence-pill__dot"></span>
|
|
13
|
+
{{this.driverStatusLabel}}
|
|
14
|
+
{{#if this.hasDriverLocationAge}}
|
|
15
|
+
·
|
|
16
|
+
{{format-duration @resource.tracker_data.driver.location_age_seconds}}
|
|
17
|
+
{{/if}}
|
|
18
|
+
</span>
|
|
19
|
+
{{#if @resource.tracker_data.fallback_provider}}
|
|
20
|
+
<span class="tracking-intelligence-pill tracking-intelligence-pill--muted">Fallback:
|
|
21
|
+
{{this.fallbackProviderLabel}}</span>
|
|
22
|
+
{{/if}}
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<TrackingStopProgress @stops={{@resource.tracker_data.stops}} @activeStop={{@resource.tracker_data.active_stop}} />
|
|
26
|
+
|
|
27
|
+
{{#if this.operatorWarning}}
|
|
28
|
+
<div class="tracking-intelligence-alert {{if (eq this.driverSignal 'Missing') 'tracking-intelligence-alert--critical'}}">
|
|
29
|
+
<div class="tracking-intelligence-alert__icon"><FaIcon @icon="triangle-exclamation" @size="xs" /></div>
|
|
30
|
+
<div class="min-w-0">
|
|
31
|
+
<div class="tracking-intelligence-alert__title">{{this.operatorWarningTitle}}</div>
|
|
32
|
+
<div class="tracking-intelligence-alert__body">{{this.operatorWarning}}</div>
|
|
33
|
+
</div>
|
|
34
|
+
{{#if this.canAssignDriver}}
|
|
35
|
+
<button type="button" class="tracking-intelligence-alert__cta" {{on "click" this.assignDriver}}>
|
|
36
|
+
<FaIcon @icon="user-plus" @size="xs" />
|
|
37
|
+
<span>Assign driver</span>
|
|
38
|
+
</button>
|
|
39
|
+
{{else if this.canPingDriver}}
|
|
40
|
+
<button type="button" class="tracking-intelligence-alert__cta" disabled={{this.pingDriver.isRunning}} {{on "click" (perform this.pingDriver)}}>
|
|
41
|
+
<FaIcon @icon="bell" @size="xs" />
|
|
42
|
+
<span>{{if this.pingDriver.isRunning "Pinging..." "Ping driver app"}}</span>
|
|
43
|
+
</button>
|
|
44
|
+
{{/if}}
|
|
45
|
+
</div>
|
|
46
|
+
{{/if}}
|
|
47
|
+
|
|
48
|
+
<div class="tracking-intelligence__eta-grid">
|
|
49
|
+
<div class="tracking-intelligence-cell tracking-intelligence-cell--accent">
|
|
50
|
+
<div class="tracking-intelligence-cell__label">
|
|
51
|
+
Smart adjusted ETA
|
|
52
|
+
<span class="tracking-intelligence-tag">Live</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="tracking-intelligence-cell__value">
|
|
55
|
+
{{#if this.hasSmartAdjustedEta}}
|
|
56
|
+
{{format-duration this.smartAdjustedEtaSeconds}}
|
|
57
|
+
{{else}}
|
|
58
|
+
{{this.smartAdjustedEtaUnavailableLabel}}
|
|
59
|
+
{{/if}}
|
|
60
|
+
</div>
|
|
61
|
+
<div class="tracking-intelligence-cell__sub">Based on provider route and driver signal</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="tracking-intelligence-cell">
|
|
64
|
+
<div class="tracking-intelligence-cell__label">Reported ETA</div>
|
|
65
|
+
<div class="tracking-intelligence-cell__value {{if this.isReportedEtaUntrusted 'tracking-intelligence-cell__value--muted'}}">
|
|
66
|
+
{{#if this.hasDisplayedReportedEta}}
|
|
67
|
+
{{format-duration this.displayedReportedEtaSeconds}}
|
|
68
|
+
{{else}}
|
|
69
|
+
{{this.etaUnavailableLabel}}
|
|
70
|
+
{{/if}}
|
|
71
|
+
</div>
|
|
72
|
+
<div class="tracking-intelligence-cell__sub {{if this.isReportedEtaUntrusted 'tracking-intelligence-cell__sub--warn'}}">{{this.reportedEtaWarning}}</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="tracking-intelligence-confidence">
|
|
77
|
+
<div class="tracking-intelligence-confidence__label">ETA confidence</div>
|
|
78
|
+
<div class="tracking-intelligence-confidence__segments {{this.confidenceToneClass}}">
|
|
79
|
+
{{#each this.confidenceSegments as |segment|}}
|
|
80
|
+
<span class="{{if segment.lit 'is-lit'}}"></span>
|
|
81
|
+
{{/each}}
|
|
82
|
+
</div>
|
|
83
|
+
<div class="tracking-intelligence-confidence__value">{{this.confidencePercent}}%</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="tracking-intelligence-destination">
|
|
87
|
+
<div class="tracking-intelligence-destination__marker">{{this.activeStopMarkerLabel}}</div>
|
|
88
|
+
<div class="min-w-0 flex-1">
|
|
89
|
+
<div class="tracking-intelligence-destination__label">{{this.activeStopLabel}}</div>
|
|
90
|
+
<div class="tracking-intelligence-destination__address">{{n-a @resource.tracker_data.active_stop.address}}</div>
|
|
91
|
+
<div class="tracking-intelligence-destination__eta">
|
|
92
|
+
ETA:
|
|
93
|
+
<strong>
|
|
94
|
+
{{#if this.hasActiveEta}}
|
|
95
|
+
{{format-duration this.activeEtaSeconds}}
|
|
96
|
+
{{else}}
|
|
97
|
+
{{this.etaUnavailableLabel}}
|
|
98
|
+
{{/if}}
|
|
99
|
+
</strong>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div class="tracking-intelligence-distance">
|
|
105
|
+
<div class="tracking-intelligence-distance__row">
|
|
106
|
+
<span>Reported total</span>
|
|
107
|
+
<strong>
|
|
108
|
+
{{#if this.hasRemainingDistance}}
|
|
109
|
+
{{format-meters @resource.tracker_data.route.distance_m}}
|
|
110
|
+
{{else}}
|
|
111
|
+
-
|
|
112
|
+
{{/if}}
|
|
113
|
+
</strong>
|
|
114
|
+
<div class="tracking-intelligence-distance__bar">
|
|
115
|
+
<span style={{this.totalProgressStyle}}></span>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="tracking-intelligence-distance__row">
|
|
119
|
+
<span>Between stops</span>
|
|
120
|
+
<strong>
|
|
121
|
+
{{#if this.hasCurrentLegDistance}}
|
|
122
|
+
{{format-meters this.currentLeg.distance_m}}
|
|
123
|
+
{{else}}
|
|
124
|
+
-
|
|
125
|
+
{{/if}}
|
|
126
|
+
</strong>
|
|
127
|
+
<div class="tracking-intelligence-distance__bar tracking-intelligence-distance__bar--secondary">
|
|
128
|
+
<span style={{this.currentLegProgressStyle}}></span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="tracking-intelligence-distance__foot">Provider context:
|
|
132
|
+
{{this.providerLabel}}
|
|
133
|
+
route</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<details class="tracking-intelligence-diagnostics">
|
|
137
|
+
<summary>
|
|
138
|
+
<span>Diagnostics</span>
|
|
139
|
+
<span class="tracking-intelligence-diagnostics__summary">
|
|
140
|
+
{{this.diagnosticsSummaryLabel}}
|
|
141
|
+
<FaIcon @icon="chevron-right" @size="xs" />
|
|
142
|
+
</span>
|
|
143
|
+
</summary>
|
|
144
|
+
<div class="tracking-intelligence-diagnostics__body">
|
|
145
|
+
{{#each this.diagnostics as |item|}}
|
|
146
|
+
<div class="tracking-intelligence-diagnostics__row">
|
|
147
|
+
<span>{{item.label}}</span>
|
|
148
|
+
<strong>{{item.value}}</strong>
|
|
149
|
+
</div>
|
|
150
|
+
{{/each}}
|
|
151
|
+
{{#if this.hasWarnings}}
|
|
152
|
+
<div class="tracking-intelligence-diagnostics__warnings">
|
|
153
|
+
{{#each @resource.tracker_data.warnings as |warning|}}
|
|
154
|
+
<span>{{warning}}</span>
|
|
155
|
+
{{/each}}
|
|
156
|
+
</div>
|
|
157
|
+
{{/if}}
|
|
24
158
|
</div>
|
|
159
|
+
</details>
|
|
160
|
+
|
|
161
|
+
<div class="tracking-intelligence-footer">
|
|
162
|
+
<span>Updated {{n-a (format-date-fns @resource.tracker_data.generated_at "d MMM HH:mm")}}</span>
|
|
163
|
+
<button type="button" class="tracking-intelligence-footer__button" disabled={{this.loadTrackerData.isRunning}} {{on "click" (perform this.loadTrackerData)}}>
|
|
164
|
+
<FaIcon @icon="rotate" @size="xs" class={{if this.loadTrackerData.isRunning "fa-spin"}} />
|
|
165
|
+
<span>{{if this.loadTrackerData.isRunning "Recalculating" "Recalculate"}}</span>
|
|
166
|
+
</button>
|
|
25
167
|
</div>
|
|
26
168
|
</div>
|
|
27
169
|
{{/if}}
|
|
28
|
-
|
|
29
|
-
|
|
170
|
+
|
|
171
|
+
<div class="text-xs uppercase tracking-wide font-semibold text-gray-500 dark:text-gray-400 mb-2">Labels</div>
|
|
172
|
+
<div class="tracking-intelligence-labels">
|
|
173
|
+
<div class="flex flex-row items-center justify-center space-x-3">
|
|
30
174
|
<div class="rounded-md bg-white p-2">
|
|
31
|
-
<img src={{concat "data:image/png;base64," @resource.tracking_number.qr_code}} class="w-
|
|
175
|
+
<img src={{concat "data:image/png;base64," @resource.tracking_number.qr_code}} class="w-12 h-12" alt={{@resource.public_id}} />
|
|
32
176
|
</div>
|
|
33
177
|
<div class="rounded-md bg-white p-2">
|
|
34
|
-
<img src={{concat "data:image/png;base64," @resource.tracking_number.barcode}} class="w-
|
|
178
|
+
<img src={{concat "data:image/png;base64," @resource.tracking_number.barcode}} class="w-36 h-12" alt={{@resource.public_id}} />
|
|
35
179
|
</div>
|
|
36
180
|
</div>
|
|
37
|
-
<div class="mt-
|
|
181
|
+
<div class="mt-2 flex justify-center items-center">
|
|
38
182
|
<Button @size="xs" @type="default" @icon="barcode" @text="Get Order Label" @onClick={{fn this.orderActions.viewLabel @resource}} @permission="fleet-ops view order" />
|
|
39
183
|
</div>
|
|
40
184
|
</div>
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
2
|
import { inject as service } from '@ember/service';
|
|
3
|
+
import { action } from '@ember/object';
|
|
3
4
|
import { debug } from '@ember/debug';
|
|
4
5
|
import { task } from 'ember-concurrency';
|
|
5
6
|
|
|
6
7
|
export default class OrderDetailsTrackingComponent extends Component {
|
|
7
8
|
@service orderActions;
|
|
9
|
+
@service fetch;
|
|
10
|
+
@service notifications;
|
|
8
11
|
|
|
9
12
|
constructor() {
|
|
10
13
|
super(...arguments);
|
|
@@ -12,10 +15,464 @@ export default class OrderDetailsTrackingComponent extends Component {
|
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
@task *loadTrackerData() {
|
|
18
|
+
if (!this.args.resource || typeof this.args.resource.loadTrackerData !== 'function') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
try {
|
|
16
23
|
yield this.args.resource.loadTrackerData();
|
|
17
24
|
} catch (err) {
|
|
18
25
|
debug('Failed to load order tracker data: ' + err.message);
|
|
19
26
|
}
|
|
20
27
|
}
|
|
28
|
+
|
|
29
|
+
get trackerData() {
|
|
30
|
+
return this.args.resource?.tracker_data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get hasTrackerData() {
|
|
34
|
+
return Boolean(this.trackerData);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get activeEtaSeconds() {
|
|
38
|
+
return this.trackerData?.eta?.active_stop_seconds;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get hasActiveEta() {
|
|
42
|
+
return this.activeEtaSeconds !== null && this.activeEtaSeconds !== undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get smartAdjustedEtaSeconds() {
|
|
46
|
+
return (
|
|
47
|
+
this.firstPositiveNumber(this.activeEtaSeconds) ??
|
|
48
|
+
this.firstPositiveNumber(this.trackerData?.route?.duration_in_traffic_s) ??
|
|
49
|
+
this.firstPositiveNumber(this.trackerData?.route?.duration_s) ??
|
|
50
|
+
this.firstPositiveNumber(this.reportedEtaSeconds) ??
|
|
51
|
+
null
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get hasSmartAdjustedEta() {
|
|
56
|
+
return this.smartAdjustedEtaSeconds !== null && this.smartAdjustedEtaSeconds !== undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get smartAdjustedEtaUnavailableLabel() {
|
|
60
|
+
if (this.driverSignal === 'Unassigned') {
|
|
61
|
+
return 'Pending driver assignment';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (this.driverSignal === 'Missing' || this.driverSignal === 'Stale') {
|
|
65
|
+
return 'Pending GPS fix';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return 'Pending start';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get hasCompletionEta() {
|
|
72
|
+
return Boolean(this.trackerData?.eta?.completion_at);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get hasRemainingDistance() {
|
|
76
|
+
return this.trackerData?.route?.distance_m !== null && this.trackerData?.route?.distance_m !== undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get hasAssignedDriver() {
|
|
80
|
+
const order = this.args.resource;
|
|
81
|
+
|
|
82
|
+
return Boolean(order?.driver_assigned || order?.driver_assigned_uuid);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get driverSignal() {
|
|
86
|
+
const trackerData = this.trackerData;
|
|
87
|
+
|
|
88
|
+
if (!this.hasAssignedDriver) {
|
|
89
|
+
return 'Unassigned';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!trackerData?.driver?.location) {
|
|
93
|
+
return 'Missing';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (trackerData?.insights?.is_location_stale) {
|
|
97
|
+
return 'Stale';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return trackerData?.driver?.online ? 'Live' : 'Offline';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get driverStatusLabel() {
|
|
104
|
+
switch (this.driverSignal) {
|
|
105
|
+
case 'Live':
|
|
106
|
+
return 'Driver live';
|
|
107
|
+
case 'Stale':
|
|
108
|
+
return 'Driver stale';
|
|
109
|
+
case 'Missing':
|
|
110
|
+
return 'Driver missing GPS';
|
|
111
|
+
case 'Unassigned':
|
|
112
|
+
return 'No driver assigned';
|
|
113
|
+
default:
|
|
114
|
+
return 'Driver offline';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get hasDriverLocationAge() {
|
|
119
|
+
return this.trackerData?.driver?.location_age_seconds !== null && this.trackerData?.driver?.location_age_seconds !== undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get driverSignalClass() {
|
|
123
|
+
switch (this.driverSignal) {
|
|
124
|
+
case 'Live':
|
|
125
|
+
return 'text-green-600 dark:text-green-400';
|
|
126
|
+
case 'Stale':
|
|
127
|
+
return 'text-yellow-600 dark:text-yellow-400';
|
|
128
|
+
case 'Missing':
|
|
129
|
+
return 'text-red-600 dark:text-red-400';
|
|
130
|
+
case 'Unassigned':
|
|
131
|
+
return 'text-yellow-600 dark:text-yellow-400';
|
|
132
|
+
default:
|
|
133
|
+
return 'text-gray-600 dark:text-gray-300';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get routeQualityItems() {
|
|
138
|
+
const trackerData = this.trackerData;
|
|
139
|
+
|
|
140
|
+
if (!trackerData) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const items = [`${this.humanize(trackerData.provider)} route`];
|
|
145
|
+
|
|
146
|
+
if (trackerData.confidence) {
|
|
147
|
+
items.push(`${this.humanize(trackerData.confidence)} confidence`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (trackerData.fallback_provider) {
|
|
151
|
+
items.push(`Fallback: ${this.humanize(trackerData.fallback_provider)}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return items;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get confidenceLabel() {
|
|
158
|
+
return this.humanize(this.trackerData?.confidence || 'unknown');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get confidencePercent() {
|
|
162
|
+
const score = this.trackerData?.confidence_score ?? this.trackerData?.confidence_percent ?? this.trackerData?.confidence_percentage;
|
|
163
|
+
|
|
164
|
+
if (score !== null && score !== undefined && !Number.isNaN(Number(score))) {
|
|
165
|
+
return Math.max(0, Math.min(100, Math.round(Number(score))));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
switch (this.trackerData?.confidence) {
|
|
169
|
+
case 'high':
|
|
170
|
+
return 92;
|
|
171
|
+
case 'medium':
|
|
172
|
+
return 68;
|
|
173
|
+
case 'low':
|
|
174
|
+
return 34;
|
|
175
|
+
default:
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
get providerLabel() {
|
|
181
|
+
return this.humanize(this.trackerData?.provider);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
get fallbackProviderLabel() {
|
|
185
|
+
return this.humanize(this.trackerData?.fallback_provider);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
get confidenceToneClass() {
|
|
189
|
+
switch (this.trackerData?.confidence) {
|
|
190
|
+
case 'high':
|
|
191
|
+
return 'tracking-intelligence-pill--good';
|
|
192
|
+
case 'medium':
|
|
193
|
+
return 'tracking-intelligence-pill--warn';
|
|
194
|
+
case 'low':
|
|
195
|
+
return 'tracking-intelligence-pill--bad';
|
|
196
|
+
default:
|
|
197
|
+
return 'tracking-intelligence-pill--muted';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
get confidenceSegments() {
|
|
202
|
+
const confidence = this.trackerData?.confidence;
|
|
203
|
+
const litCount = confidence === 'high' ? 5 : confidence === 'medium' ? 3 : confidence === 'low' ? 2 : 1;
|
|
204
|
+
|
|
205
|
+
return Array.from({ length: 5 }, (_, index) => ({
|
|
206
|
+
lit: index < litCount,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get activeStopIndex() {
|
|
211
|
+
const stops = this.trackerData?.stops ?? [];
|
|
212
|
+
const activeStop = this.trackerData?.active_stop;
|
|
213
|
+
const index = stops.findIndex((stop) => this.matchesStop(stop, activeStop));
|
|
214
|
+
|
|
215
|
+
return index >= 0 ? index + 1 : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
get totalStops() {
|
|
219
|
+
return this.trackerData?.stops?.length ?? 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
get activeStopLabel() {
|
|
223
|
+
if (!this.activeStopIndex || !this.totalStops) {
|
|
224
|
+
return 'NOW HEADING TO';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return `NOW HEADING TO - STOP ${this.activeStopIndex} OF ${this.totalStops}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
get activeStopMarkerLabel() {
|
|
231
|
+
const activeStop = this.trackerData?.active_stop;
|
|
232
|
+
|
|
233
|
+
if (activeStop?.type === 'pickup') {
|
|
234
|
+
return 'P';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (activeStop?.type === 'dropoff') {
|
|
238
|
+
return 'D';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return this.activeStopIndex ?? '•';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
get reportedEtaSeconds() {
|
|
245
|
+
const activeStop = this.trackerData?.active_stop;
|
|
246
|
+
const eta = this.args.resource?.eta ?? {};
|
|
247
|
+
|
|
248
|
+
return activeStop?.eta_seconds ?? eta?.[activeStop?.id] ?? eta?.[activeStop?.uuid] ?? eta?.[activeStop?.public_id] ?? null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
get displayedReportedEtaSeconds() {
|
|
252
|
+
return (
|
|
253
|
+
this.firstPositiveNumber(this.reportedEtaSeconds) ??
|
|
254
|
+
this.firstPositiveNumber(this.activeEtaSeconds) ??
|
|
255
|
+
this.firstPositiveNumber(this.trackerData?.route?.duration_in_traffic_s) ??
|
|
256
|
+
this.firstPositiveNumber(this.trackerData?.route?.duration_s) ??
|
|
257
|
+
null
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
get hasDisplayedReportedEta() {
|
|
262
|
+
return this.displayedReportedEtaSeconds !== null && this.displayedReportedEtaSeconds !== undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
get isReportedEtaUntrusted() {
|
|
266
|
+
const trackerData = this.trackerData;
|
|
267
|
+
|
|
268
|
+
if (!trackerData) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return !trackerData?.driver?.location || trackerData?.insights?.is_location_stale || trackerData.fallback_provider || (trackerData.confidence && trackerData.confidence !== 'high');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get reportedEtaWarning() {
|
|
276
|
+
if (!this.isReportedEtaUntrusted) {
|
|
277
|
+
return 'Reported from existing route data';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return this.operatorWarning ?? 'Reported ETA may not reflect the latest tracking signal.';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get etaUnavailableLabel() {
|
|
284
|
+
if (this.driverSignal === 'Unassigned') {
|
|
285
|
+
return 'Pending driver assignment';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (this.driverSignal === 'Missing' || this.driverSignal === 'Stale') {
|
|
289
|
+
return 'Pending GPS fix';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return '-';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
get operatorWarningTitle() {
|
|
296
|
+
switch (this.driverSignal) {
|
|
297
|
+
case 'Unassigned':
|
|
298
|
+
return 'No driver assigned';
|
|
299
|
+
case 'Missing':
|
|
300
|
+
return 'Pending GPS fix';
|
|
301
|
+
case 'Stale':
|
|
302
|
+
return 'Stale driver location';
|
|
303
|
+
default:
|
|
304
|
+
return 'Tracking estimate warning';
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
get canAssignDriver() {
|
|
309
|
+
return this.driverSignal === 'Unassigned';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
get canPingDriver() {
|
|
313
|
+
return this.driverSignal === 'Missing' || this.driverSignal === 'Stale';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
get warningsCount() {
|
|
317
|
+
return (this.trackerData?.warnings ?? []).length;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
get diagnosticsSummaryLabel() {
|
|
321
|
+
return this.warningsCount === 1 ? '1 warning' : `${this.warningsCount} warnings`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
get totalProgressPercentage() {
|
|
325
|
+
const percentage = Number(this.trackerData?.progress?.percentage);
|
|
326
|
+
|
|
327
|
+
if (Number.isFinite(percentage)) {
|
|
328
|
+
return Math.max(0, Math.min(100, percentage));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const stops = this.trackerData?.stops ?? [];
|
|
332
|
+
const completedStops = stops.filter((stop) => stop.completed).length;
|
|
333
|
+
|
|
334
|
+
if (stops.length) {
|
|
335
|
+
return Math.max(0, Math.min(100, Math.round((completedStops / stops.length) * 100)));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
get totalProgressStyle() {
|
|
342
|
+
const percentage = this.hasRemainingDistance && this.totalProgressPercentage === 0 ? 2 : this.totalProgressPercentage;
|
|
343
|
+
|
|
344
|
+
return `width: ${percentage}%;`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
get currentLeg() {
|
|
348
|
+
return this.trackerData?.route?.legs?.[0] ?? null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
get hasCurrentLegDistance() {
|
|
352
|
+
return this.currentLeg?.distance_m !== null && this.currentLeg?.distance_m !== undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
get currentLegProgressPercentage() {
|
|
356
|
+
const explicit = Number(this.currentLeg?.progress_percentage ?? this.trackerData?.progress?.active_leg_percentage);
|
|
357
|
+
|
|
358
|
+
if (Number.isFinite(explicit)) {
|
|
359
|
+
return Math.max(0, Math.min(100, explicit));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (this.driverSignal === 'Unassigned' || this.driverSignal === 'Missing') {
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (this.driverSignal === 'Stale') {
|
|
367
|
+
return 18;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return Math.max(8, Math.min(92, this.totalProgressPercentage));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
get currentLegProgressStyle() {
|
|
374
|
+
return `width: ${this.currentLegProgressPercentage}%;`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
get hasWarnings() {
|
|
378
|
+
return (this.trackerData?.warnings ?? []).length > 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
get diagnostics() {
|
|
382
|
+
const trackerData = this.trackerData;
|
|
383
|
+
|
|
384
|
+
if (!trackerData) {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return [
|
|
389
|
+
{ label: 'Provider', value: this.humanize(trackerData.provider) },
|
|
390
|
+
{ label: 'Fallback', value: trackerData.fallback_provider ? this.humanize(trackerData.fallback_provider) : 'No' },
|
|
391
|
+
{ label: 'Traffic aware', value: trackerData.options?.traffic_enabled ? 'Yes' : 'No' },
|
|
392
|
+
{ label: 'Confidence', value: this.confidenceLabel },
|
|
393
|
+
{ label: 'Driver signal', value: this.driverSignal },
|
|
394
|
+
{ label: 'Warnings', value: String((trackerData.warnings ?? []).length) },
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
get operatorWarning() {
|
|
399
|
+
const trackerData = this.trackerData;
|
|
400
|
+
|
|
401
|
+
if (!trackerData) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!this.hasAssignedDriver) {
|
|
406
|
+
return 'Assign a driver to start live tracking and improve ETA accuracy.';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!trackerData?.driver?.location) {
|
|
410
|
+
return 'Driver location is missing, so ETA accuracy may be limited.';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (trackerData?.insights?.is_location_stale) {
|
|
414
|
+
return 'Driver location is stale. ETA may not reflect the latest movement.';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (trackerData.fallback_provider) {
|
|
418
|
+
return `Using ${this.humanize(trackerData.fallback_provider)} fallback because the preferred tracking provider was unavailable.`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (trackerData.confidence && trackerData.confidence !== 'high') {
|
|
422
|
+
return `${this.humanize(trackerData.confidence)} confidence ETA. Treat the estimate as directional.`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if ((trackerData.warnings ?? []).some((warning) => String(warning).startsWith('provider_failed'))) {
|
|
426
|
+
return 'Tracking provider returned an error. Showing the best available estimate.';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
humanize(value) {
|
|
433
|
+
return String(value ?? '')
|
|
434
|
+
.replace(/[_:-]+/g, ' ')
|
|
435
|
+
.replace(/\s+/g, ' ')
|
|
436
|
+
.trim()
|
|
437
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
firstPositiveNumber(value) {
|
|
441
|
+
const number = Number(value);
|
|
442
|
+
|
|
443
|
+
return Number.isFinite(number) && number > 0 ? number : null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
matchesStop(stop, activeStop) {
|
|
447
|
+
if (!stop || !activeStop) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return stop.uuid === activeStop.uuid || stop.public_id === activeStop.public_id || stop.id === activeStop.id;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
@action assignDriver() {
|
|
455
|
+
const order = this.args.resource;
|
|
456
|
+
|
|
457
|
+
if (!order || typeof this.orderActions.assignDriver !== 'function') {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return this.orderActions.assignDriver(order);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
@task *pingDriver() {
|
|
465
|
+
const order = this.args.resource;
|
|
466
|
+
|
|
467
|
+
if (!order) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
yield this.fetch.post(`orders/${order.id}/ping-driver`);
|
|
473
|
+
this.notifications.success('Driver app ping sent.');
|
|
474
|
+
} catch (err) {
|
|
475
|
+
this.notifications.error(err.message ?? 'Unable to ping driver app.');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
21
478
|
}
|