@fleetbase/fleetops-engine 0.6.47 → 0.6.49

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.
Files changed (223) hide show
  1. package/addon/components/admin/routing-settings.hbs +71 -0
  2. package/addon/components/admin/routing-settings.js +124 -0
  3. package/addon/components/customer/orders.js +1 -1
  4. package/addon/components/display-place.hbs +1 -1
  5. package/addon/components/driver/details.hbs +5 -1
  6. package/addon/components/driver/form.hbs +11 -1
  7. package/addon/components/fleet-listing-panel.hbs +2 -2
  8. package/addon/components/fleet-listing-panel.js +10 -0
  9. package/addon/components/map/google-live-map.js +12 -73
  10. package/addon/components/map/leaflet-live-map.hbs +149 -33
  11. package/addon/components/map/order-list-overlay/order.hbs +18 -3
  12. package/addon/components/map/toolbar.hbs +2 -2
  13. package/addon/components/map/toolbar.js +10 -2
  14. package/addon/components/orchestrator/phase-builder.hbs +49 -5
  15. package/addon/components/orchestrator/phase-builder.js +34 -1
  16. package/addon/components/orchestrator/resource-panel.hbs +19 -1
  17. package/addon/components/orchestrator/resource-panel.js +4 -0
  18. package/addon/components/orchestrator-workbench.js +4 -1
  19. package/addon/components/order/details/purchase-rate.hbs +19 -22
  20. package/addon/components/order/details/route.hbs +26 -1
  21. package/addon/components/order/details/route.js +50 -0
  22. package/addon/components/order/details/tracking.hbs +171 -27
  23. package/addon/components/order/details/tracking.js +457 -0
  24. package/addon/components/order/form.hbs +2 -2
  25. package/addon/components/order/kanban-card.hbs +18 -3
  26. package/addon/components/order-list-overlay/order.hbs +18 -3
  27. package/addon/components/order-progress-card.hbs +21 -6
  28. package/addon/components/order-tracking-lookup.hbs +7 -7
  29. package/addon/components/order-tracking-lookup.js +1 -1
  30. package/addon/components/route-list.hbs +78 -8
  31. package/addon/components/route-list.js +48 -6
  32. package/addon/components/tracking-stop-progress.hbs +45 -0
  33. package/addon/components/tracking-stop-progress.js +79 -0
  34. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  35. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  36. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  37. package/addon/components/widget/fuel-efficiency.js +43 -0
  38. package/addon/components/widget/geofence-violations.hbs +44 -0
  39. package/addon/components/widget/geofence-violations.js +55 -0
  40. package/addon/components/widget/issues-insights.hbs +43 -0
  41. package/addon/components/widget/issues-insights.js +62 -0
  42. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  43. package/addon/components/widget/kpi-active-orders.js +5 -0
  44. package/addon/components/widget/kpi-aov.hbs +10 -0
  45. package/addon/components/widget/kpi-aov.js +5 -0
  46. package/addon/components/widget/kpi-distance.hbs +10 -0
  47. package/addon/components/widget/kpi-distance.js +5 -0
  48. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  49. package/addon/components/widget/kpi-drivers-online.js +5 -0
  50. package/addon/components/widget/kpi-earnings.hbs +10 -0
  51. package/addon/components/widget/kpi-earnings.js +5 -0
  52. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  53. package/addon/components/widget/kpi-open-issues.js +5 -0
  54. package/addon/components/widget/kpi-tile.hbs +59 -0
  55. package/addon/components/widget/kpi-tile.js +168 -0
  56. package/addon/components/widget/live-fleet.hbs +122 -0
  57. package/addon/components/widget/live-fleet.js +365 -0
  58. package/addon/components/widget/maintenance-overview.hbs +45 -0
  59. package/addon/components/widget/maintenance-overview.js +40 -0
  60. package/addon/components/widget/on-time-delivery.hbs +27 -0
  61. package/addon/components/widget/on-time-delivery.js +73 -0
  62. package/addon/components/widget/operations-pulse.hbs +51 -0
  63. package/addon/components/widget/operations-pulse.js +173 -0
  64. package/addon/components/widget/orders-by-status.hbs +26 -0
  65. package/addon/components/widget/orders-by-status.js +54 -0
  66. package/addon/components/widget/revenue-trend.hbs +34 -0
  67. package/addon/components/widget/revenue-trend.js +67 -0
  68. package/addon/components/widget/top-drivers.hbs +44 -0
  69. package/addon/components/widget/top-drivers.js +54 -0
  70. package/addon/controllers/settings/routing.js +124 -1
  71. package/addon/extension.js +172 -3
  72. package/addon/helpers/format-duration.js +38 -0
  73. package/addon/routes/application.js +6 -0
  74. package/addon/services/map-adapter/google.js +13 -2
  75. package/addon/services/order-list-overlay.js +2 -2
  76. package/addon/styles/fleetops-engine.css +806 -46
  77. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  78. package/addon/templates/maintenance/parts/index.hbs +1 -1
  79. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  80. package/addon/templates/management/vehicles/index.hbs +1 -1
  81. package/addon/templates/settings/avatars.hbs +1 -1
  82. package/addon/templates/settings/custom-fields.hbs +1 -1
  83. package/addon/templates/settings/map.hbs +1 -1
  84. package/addon/templates/settings/navigator-app.hbs +1 -1
  85. package/addon/templates/settings/notifications.hbs +1 -1
  86. package/addon/templates/settings/orchestrator.hbs +1 -1
  87. package/addon/templates/settings/payments/index.hbs +1 -1
  88. package/addon/templates/settings/payments/onboard.hbs +1 -1
  89. package/addon/templates/settings/routing.hbs +82 -4
  90. package/addon/templates/settings/scheduling.hbs +1 -1
  91. package/addon/utils/live-map-card-content.js +171 -0
  92. package/app/components/tracking-stop-progress.js +1 -0
  93. package/app/components/widget/fuel-efficiency.js +1 -0
  94. package/app/components/widget/geofence-violations.js +1 -0
  95. package/app/components/widget/issues-insights.js +1 -0
  96. package/app/components/widget/kpi-active-orders.js +1 -0
  97. package/app/components/widget/kpi-aov.js +1 -0
  98. package/app/components/widget/kpi-distance.js +1 -0
  99. package/app/components/widget/kpi-drivers-online.js +1 -0
  100. package/app/components/widget/kpi-earnings.js +1 -0
  101. package/app/components/widget/kpi-open-issues.js +1 -0
  102. package/app/components/widget/kpi-tile.js +1 -0
  103. package/app/components/widget/live-fleet.js +1 -0
  104. package/app/components/widget/maintenance-overview.js +1 -0
  105. package/app/components/widget/on-time-delivery.js +1 -0
  106. package/app/components/widget/operations-pulse.js +1 -0
  107. package/app/components/widget/orders-by-status.js +1 -0
  108. package/app/components/widget/revenue-trend.js +1 -0
  109. package/app/components/widget/top-drivers.js +1 -0
  110. package/app/helpers/format-duration.js +1 -0
  111. package/app/utils/live-map-card-content.js +1 -0
  112. package/composer.json +1 -1
  113. package/extension.json +1 -1
  114. package/package.json +3 -3
  115. package/pnpm-workspace.yaml +1 -0
  116. package/server/config/fleetops.php +32 -11
  117. package/server/config/telematics.php +111 -1
  118. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
  119. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  120. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  121. package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
  122. package/server/seeders/Testing/FleetSeeder.php +341 -0
  123. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  124. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  125. package/server/seeders/Testing/OrdersSeeder.php +214 -0
  126. package/server/seeders/Testing/TestingSeeder.php +40 -0
  127. package/server/seeders/Testing/VroomOrchestrationSeeder.php +409 -0
  128. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  129. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
  130. package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
  131. package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
  132. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
  133. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  134. package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
  135. package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
  136. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  137. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
  138. package/server/src/Http/Controllers/Internal/v1/OrderController.php +52 -21
  139. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  140. package/server/src/Http/Controllers/Internal/v1/SettingController.php +164 -12
  141. package/server/src/Http/Resources/v1/Driver.php +2 -0
  142. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  143. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  144. package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
  145. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  146. package/server/src/Models/Contact.php +122 -13
  147. package/server/src/Models/Driver.php +2 -0
  148. package/server/src/Models/Order.php +1 -1
  149. package/server/src/Models/OrderConfig.php +14 -11
  150. package/server/src/Models/Payload.php +8 -8
  151. package/server/src/Models/Place.php +0 -3
  152. package/server/src/Models/ServiceRate.php +1 -1
  153. package/server/src/Notifications/LateDeparture.php +48 -0
  154. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  155. package/server/src/Notifications/RouteDeviation.php +48 -0
  156. package/server/src/Observers/ContactObserver.php +8 -0
  157. package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
  158. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +357 -38
  159. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
  160. package/server/src/Providers/FleetOpsServiceProvider.php +32 -0
  161. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  162. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  163. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  164. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  165. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  166. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  167. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  168. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  169. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  170. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  171. package/server/src/Support/Analytics/RevenueTrend.php +79 -0
  172. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  173. package/server/src/Support/LiveOrderQuery.php +75 -0
  174. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  175. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  176. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  177. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  178. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  179. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  180. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  181. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  182. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  183. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  184. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  185. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  186. package/server/src/Support/Metrics/Registry.php +46 -0
  187. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  188. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  189. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  190. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  191. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  192. package/server/src/Support/Metrics.php +46 -234
  193. package/server/src/Support/OSRM.php +11 -6
  194. package/server/src/Support/OrderTracker.php +7 -582
  195. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  196. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  197. package/server/src/Support/Utils.php +1 -1
  198. package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
  199. package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
  200. package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
  201. package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
  202. package/server/src/Tracking/README.md +25 -0
  203. package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
  204. package/server/src/Tracking/TrackingContext.php +63 -0
  205. package/server/src/Tracking/TrackingContextBuilder.php +204 -0
  206. package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
  207. package/server/src/Tracking/TrackingOptions.php +49 -0
  208. package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
  209. package/server/src/Tracking/TrackingProviderManager.php +89 -0
  210. package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
  211. package/server/src/Tracking/TrackingProviderResult.php +20 -0
  212. package/server/src/Tracking/TrackingStop.php +59 -0
  213. package/server/src/routes.php +27 -0
  214. package/server/tests/AnalyticsRoutesTest.php +41 -0
  215. package/server/tests/CapacityAllocationEngineTest.php +108 -0
  216. package/server/tests/LiveOrderQueryTest.php +34 -0
  217. package/server/tests/MetricsRegistryTest.php +88 -0
  218. package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
  219. package/server/tests/OrchestratorConsumableApiTest.php +74 -0
  220. package/server/tests/PingDriverEndpointTest.php +19 -0
  221. package/server/tests/TrackingIntelligenceTest.php +176 -0
  222. package/server/tests/VroomOrchestrationEngineTest.php +250 -0
  223. package/translations/en-us.yaml +41 -0
@@ -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
  }
@@ -17,7 +17,7 @@
17
17
  <Order::Form::Details @resource={{@resource}} />
18
18
  <Order::Form::Route @resource={{@resource}} />
19
19
  <RegistryYield @registry="fleet-ops:component:order:form" as |RegistryComponent|>
20
- <RegistryComponent @order={{@order}} />
20
+ <RegistryComponent @order={{@resource}} @resource={{@resource}} />
21
21
  </RegistryYield>
22
22
  <Order::Form::Payload @resource={{@resource}} />
23
23
  <Order::Form::ServiceRate @resource={{@resource}} />
@@ -25,4 +25,4 @@
25
25
  <Order::Form::Documents @resource={{@resource}} />
26
26
  <Order::Form::Metadata @resource={{@resource}} />
27
27
  {{/if}}
28
- </div>
28
+ </div>
@@ -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.progress_percentage}}
19
- @firstWaypointCompleted={{@card.tracker_data.first_waypoint_completed}}
20
- @lastWaypointCompleted={{@card.tracker_data.last_waypoint_completed}}
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.progress_percentage}}
30
- @firstWaypointCompleted={{@order.tracker_data.first_waypoint_completed}}
31
- @lastWaypointCompleted={{@order.tracker_data.last_waypoint_completed}}
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.progress_percentage}}
20
- @firstWaypointCompleted={{this.order.tracker_data.first_waypoint_completed}}
21
- @lastWaypointCompleted={{this.order.tracker_data.last_waypoint_completed}}
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.current_destination_eta}}</div>
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.estimated_completion_time_formatted}}</div>
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.current_destination.address}}</div>
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.progress_percentage}}
94
- @firstWaypointCompleted={{this.order.tracker_data.first_waypoint_completed}}
95
- @lastWaypointCompleted={{this.order.tracker_data.last_waypoint_completed}}
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.current_destination_eta}}</div>
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.estimated_completion_time_formatted}}</div>
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.current_destination.address}}</div>
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.next_destination.address}}</div>
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.driver_current_location');
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];