@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.
Files changed (90) 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/map/order-list-overlay/order.hbs +18 -3
  6. package/addon/components/map/toolbar.hbs +2 -2
  7. package/addon/components/map/toolbar.js +10 -2
  8. package/addon/components/orchestrator/phase-builder.hbs +28 -5
  9. package/addon/components/orchestrator/phase-builder.js +21 -1
  10. package/addon/components/orchestrator/resource-panel.hbs +19 -1
  11. package/addon/components/orchestrator/resource-panel.js +4 -0
  12. package/addon/components/orchestrator-workbench.js +3 -1
  13. package/addon/components/order/details/purchase-rate.hbs +19 -22
  14. package/addon/components/order/details/route.hbs +26 -1
  15. package/addon/components/order/details/route.js +50 -0
  16. package/addon/components/order/details/tracking.hbs +171 -27
  17. package/addon/components/order/details/tracking.js +457 -0
  18. package/addon/components/order/kanban-card.hbs +18 -3
  19. package/addon/components/order-list-overlay/order.hbs +18 -3
  20. package/addon/components/order-progress-card.hbs +21 -6
  21. package/addon/components/order-tracking-lookup.hbs +7 -7
  22. package/addon/components/order-tracking-lookup.js +1 -1
  23. package/addon/components/route-list.hbs +78 -8
  24. package/addon/components/route-list.js +48 -6
  25. package/addon/components/tracking-stop-progress.hbs +45 -0
  26. package/addon/components/tracking-stop-progress.js +79 -0
  27. package/addon/controllers/settings/routing.js +98 -1
  28. package/addon/helpers/format-duration.js +38 -0
  29. package/addon/routes/application.js +6 -0
  30. package/addon/services/order-list-overlay.js +2 -2
  31. package/addon/styles/fleetops-engine.css +733 -0
  32. package/addon/templates/settings/routing.hbs +60 -2
  33. package/app/components/tracking-stop-progress.js +1 -0
  34. package/app/helpers/format-duration.js +1 -0
  35. package/composer.json +1 -1
  36. package/extension.json +1 -1
  37. package/package.json +1 -1
  38. package/server/config/fleetops.php +32 -11
  39. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
  40. package/server/seeders/VroomOrchestrationSeeder.php +402 -0
  41. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
  42. package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
  43. package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
  44. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
  45. package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
  46. package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
  47. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
  48. package/server/src/Http/Controllers/Internal/v1/OrderController.php +40 -15
  49. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  50. package/server/src/Http/Controllers/Internal/v1/SettingController.php +119 -12
  51. package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
  52. package/server/src/Models/Contact.php +122 -13
  53. package/server/src/Models/Order.php +1 -1
  54. package/server/src/Models/Payload.php +8 -8
  55. package/server/src/Models/Place.php +0 -3
  56. package/server/src/Models/ServiceRate.php +1 -1
  57. package/server/src/Observers/ContactObserver.php +8 -0
  58. package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
  59. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +330 -38
  60. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
  61. package/server/src/Providers/FleetOpsServiceProvider.php +27 -0
  62. package/server/src/Support/LiveOrderQuery.php +75 -0
  63. package/server/src/Support/Metrics.php +15 -0
  64. package/server/src/Support/OSRM.php +11 -6
  65. package/server/src/Support/OrderTracker.php +7 -582
  66. package/server/src/Support/Utils.php +1 -1
  67. package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
  68. package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
  69. package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
  70. package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
  71. package/server/src/Tracking/README.md +25 -0
  72. package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
  73. package/server/src/Tracking/TrackingContext.php +63 -0
  74. package/server/src/Tracking/TrackingContextBuilder.php +204 -0
  75. package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
  76. package/server/src/Tracking/TrackingOptions.php +49 -0
  77. package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
  78. package/server/src/Tracking/TrackingProviderManager.php +89 -0
  79. package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
  80. package/server/src/Tracking/TrackingProviderResult.php +20 -0
  81. package/server/src/Tracking/TrackingStop.php +59 -0
  82. package/server/src/routes.php +11 -0
  83. package/server/tests/CapacityAllocationEngineTest.php +108 -0
  84. package/server/tests/LiveOrderQueryTest.php +34 -0
  85. package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
  86. package/server/tests/OrchestratorConsumableApiTest.php +74 -0
  87. package/server/tests/PingDriverEndpointTest.php +19 -0
  88. package/server/tests/TrackingIntelligenceTest.php +176 -0
  89. package/server/tests/VroomOrchestrationEngineTest.php +159 -0
  90. 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.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];
@@ -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 class="index-count fleetops-route-stop-badge" style={{this.firstStop.badgeStyle}}>{{this.firstStop.label}}</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
- <DisplayPlace @place={{this.firstStop.place}} @eta={{get @eta this.firstStop.place.id}} @waypointActions={{@waypointActions}} />
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
- <DisplayPlace @place={{stop.place}} @eta={{get @eta stop.place.id}} @waypointActions={{@waypointActions}} />
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
- <DisplayPlace @place={{stop.place}} @eta={{get @eta stop.place.id}} @waypointActions={{@waypointActions}} />
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 class="index-count fleetops-route-stop-badge" style={{this.lastStop.badgeStyle}}>{{this.lastStop.label}}</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
- <DisplayPlace @place={{this.lastStop.place}} @eta={{get @eta this.lastStop.place.id}} @waypointActions={{@waypointActions}} />
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.map((routePoint) => ({
20
- routePoint,
21
- place: routePoint.place,
22
- badgeStyle: this.badgeStyleForStop(routePoint),
23
- ...describeRoutePoint(routePoint, this.routeColor),
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.notifications.success('Routing setting saved.');
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
+ });