@fleetbase/fleetops-engine 0.6.49 → 0.6.50

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 (142) hide show
  1. package/addon/components/customer/create-order-form.hbs +2 -2
  2. package/addon/components/customer/form.hbs +2 -2
  3. package/addon/components/customer/order-form.hbs +2 -2
  4. package/addon/components/customer/orders.hbs +2 -2
  5. package/addon/components/display-place.hbs +2 -2
  6. package/addon/components/map/google-live-map.js +6 -2
  7. package/addon/components/map/leaflet-live-map.hbs +48 -14
  8. package/addon/components/map/leaflet-live-map.js +184 -20
  9. package/addon/components/modals/order-change-destination.hbs +46 -0
  10. package/addon/components/modals/order-change-destination.js +106 -0
  11. package/addon/components/modals/stop-activity.hbs +28 -0
  12. package/addon/components/modals/stop-activity.js +52 -0
  13. package/addon/components/modals/update-order-activity.hbs +45 -3
  14. package/addon/components/modals/update-order-activity.js +143 -5
  15. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  16. package/addon/components/orchestrator/resource-panel.js +2 -4
  17. package/addon/components/order/activity-list.hbs +2 -2
  18. package/addon/components/order/activity-timeline.hbs +31 -29
  19. package/addon/components/order/details/activity.hbs +3 -3
  20. package/addon/components/order/details/activity.js +11 -0
  21. package/addon/components/order/details/documents.js +2 -0
  22. package/addon/components/order/details/purchase-rate.hbs +2 -2
  23. package/addon/components/order/details/route.hbs +11 -3
  24. package/addon/components/order/details/route.js +91 -14
  25. package/addon/components/order/details/tracking.hbs +101 -72
  26. package/addon/components/order/details/tracking.js +149 -2
  27. package/addon/components/order/form/details.hbs +3 -3
  28. package/addon/components/order/form/documents.js +11 -4
  29. package/addon/components/order/form/route.hbs +108 -104
  30. package/addon/components/order/form/route.js +37 -1
  31. package/addon/components/order/form/service-rate.hbs +2 -2
  32. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  33. package/addon/components/order-config-manager/activity-flow.js +178 -12
  34. package/addon/components/place/address.js +1 -1
  35. package/addon/components/route-list.hbs +17 -17
  36. package/addon/components/route-list.js +42 -10
  37. package/addon/components/service-rate/details.hbs +41 -0
  38. package/addon/components/service-rate/form.hbs +118 -0
  39. package/addon/components/service-rate/form.js +55 -0
  40. package/addon/components/vehicle/form.js +1 -1
  41. package/addon/components/vehicle/pill.hbs +1 -1
  42. package/addon/controllers/management/vehicles/index/new.js +1 -1
  43. package/addon/controllers/operations/orders/index/details.js +4 -4
  44. package/addon/controllers/operations/orders/index.js +14 -0
  45. package/addon/helpers/place-address-html.js +2 -2
  46. package/addon/routes/operations/service-rates/index.js +1 -1
  47. package/addon/services/geofence.js +163 -19
  48. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  49. package/addon/services/vehicle-actions.js +1 -1
  50. package/addon/styles/fleetops-engine.css +91 -1
  51. package/addon/utils/fleet-ops-options.js +2 -1
  52. package/addon/utils/normalize-order-config-flow.js +41 -0
  53. package/addon/utils/place-address-html.js +3 -2
  54. package/addon/utils/route-visualization.js +54 -7
  55. package/app/components/modals/order-change-destination.js +1 -0
  56. package/app/components/modals/stop-activity.js +1 -0
  57. package/composer.json +1 -1
  58. package/extension.json +1 -1
  59. package/package.json +3 -3
  60. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  61. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  62. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  63. package/server/resources/views/labels/default.php +57 -49
  64. package/server/resources/views/labels/waypoint-label.php +27 -20
  65. package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
  66. package/server/seeders/Testing/FleetSeeder.php +22 -22
  67. package/server/seeders/Testing/OrdersSeeder.php +6 -22
  68. package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
  69. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  70. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  71. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  72. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  73. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  74. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  75. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  76. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  77. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  78. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  79. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  80. package/server/src/Http/Controllers/Internal/v1/OrderController.php +429 -26
  81. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  82. package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
  83. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  84. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  85. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  86. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  87. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  88. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  89. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  90. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  91. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  92. package/server/src/Http/Resources/v1/Customer.php +79 -0
  93. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  94. package/server/src/Http/Resources/v1/Payload.php +23 -20
  95. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  96. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  97. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  98. package/server/src/Models/Contact.php +92 -15
  99. package/server/src/Models/Customer.php +64 -0
  100. package/server/src/Models/Driver.php +12 -3
  101. package/server/src/Models/Entity.php +2 -2
  102. package/server/src/Models/Order.php +29 -4
  103. package/server/src/Models/Payload.php +17 -1
  104. package/server/src/Models/ServiceRate.php +273 -1
  105. package/server/src/Models/ServiceRateFee.php +39 -9
  106. package/server/src/Models/TrackingNumber.php +8 -2
  107. package/server/src/Models/Vehicle.php +18 -2
  108. package/server/src/Models/Waypoint.php +2 -2
  109. package/server/src/Notifications/LateDeparture.php +2 -2
  110. package/server/src/Notifications/ProlongedStoppage.php +2 -2
  111. package/server/src/Notifications/RouteDeviation.php +2 -2
  112. package/server/src/Observers/ContactObserver.php +2 -0
  113. package/server/src/Observers/ServiceRateObserver.php +6 -6
  114. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
  115. package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
  116. package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
  117. package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
  118. package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
  119. package/server/src/Support/Analytics/RevenueTrend.php +0 -1
  120. package/server/src/Support/Analytics/TopDrivers.php +1 -1
  121. package/server/src/Support/CustomerAuth.php +98 -0
  122. package/server/src/Support/LabelPdf.php +52 -0
  123. package/server/src/Support/Metrics/AbstractMetric.php +4 -4
  124. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  125. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  126. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  127. package/server/src/Tracking/TrackingStop.php +13 -11
  128. package/server/src/routes.php +43 -9
  129. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  130. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  131. package/server/tests/CustomerEndpointTest.php +199 -0
  132. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  133. package/server/tests/LabelPdfRenderingTest.php +23 -0
  134. package/server/tests/LiveControllerViewportTest.php +60 -0
  135. package/server/tests/MetricsRegistryTest.php +4 -8
  136. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  137. package/server/tests/OrderConfigEndpointTest.php +86 -0
  138. package/server/tests/OrderFileAttachmentTest.php +20 -0
  139. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  140. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  141. package/translations/en-us.yaml +1 -0
  142. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -41,23 +41,44 @@ export default class OrderDetailsRouteComponent extends Component {
41
41
  isLoading: this.optimizeRoute.isRunning,
42
42
  },
43
43
  {
44
- type: 'default',
45
- text: 'Edit',
46
- icon: 'pencil',
47
- iconPrefix: 'fas',
48
- permission: 'fleet-ops update-route-for order',
49
- disabled: this.args.resource.status === 'canceled',
50
- onClick: () => {
51
- this.orderActions.editRoute(this.args.resource);
52
- },
44
+ icon: 'ellipsis-h',
45
+ prefix: 'fas',
46
+ triggerClass: 'fleetops-btn-xxs',
47
+ items: [
48
+ {
49
+ text: 'Edit route',
50
+ icon: 'pencil',
51
+ disabled: !this.args.resource.hasActiveStatus,
52
+ onClick: () => {
53
+ this.orderActions.editRoute(this.args.resource);
54
+ },
55
+ },
56
+ this.isMultipleWaypointOrder
57
+ ? {
58
+ text: 'Change destination',
59
+ icon: 'location-dot',
60
+ disabled: !this.args.resource.hasActiveStatus,
61
+ onClick: () => {
62
+ this.changeDestination();
63
+ },
64
+ }
65
+ : null,
66
+ ].filter(Boolean),
53
67
  },
54
- ];
68
+ ].filter(Boolean);
55
69
  }
56
70
 
57
71
  get canOptimizeRoute() {
58
72
  return canOptimizeIntermediateWaypoints(this.args.resource?.payload);
59
73
  }
60
74
 
75
+ get isMultipleWaypointOrder() {
76
+ const waypoints = this.args.resource?.payload?.waypoints;
77
+ const waypointCount = typeof waypoints?.toArray === 'function' ? waypoints.toArray().length : Array.isArray(waypoints) ? waypoints.length : (waypoints?.length ?? 0);
78
+
79
+ return Boolean(this.args.resource?.hasIntermediateWaypoints || waypointCount > 0);
80
+ }
81
+
61
82
  get showOptimizationSelect() {
62
83
  return this.routeOptimization.availableEngines.length > 1;
63
84
  }
@@ -66,6 +87,17 @@ export default class OrderDetailsRouteComponent extends Component {
66
87
  return this.args.resource?.tracker_data;
67
88
  }
68
89
 
90
+ get showRouteEtaData() {
91
+ if (this.trackerData?.lifecycle?.show_live_eta !== undefined) {
92
+ return Boolean(this.trackerData.lifecycle.show_live_eta);
93
+ }
94
+
95
+ const status = String(this.args.resource?.status ?? 'created').toLowerCase();
96
+ const hasStarted = Boolean(this.args.resource?.started ?? this.args.resource?.started_at ?? status === 'started');
97
+
98
+ return hasStarted && !['completed', 'canceled'].includes(status);
99
+ }
100
+
69
101
  get hasTrackingRouteSummary() {
70
102
  return Boolean(this.trackerData?.route || this.trackerData?.eta);
71
103
  }
@@ -75,7 +107,7 @@ export default class OrderDetailsRouteComponent extends Component {
75
107
  }
76
108
 
77
109
  get hasCompletionEta() {
78
- return Boolean(this.trackerData?.eta?.completion_at);
110
+ return this.showRouteEtaData && Boolean(this.trackerData?.eta?.completion_at);
79
111
  }
80
112
 
81
113
  get routeStopsCount() {
@@ -83,10 +115,14 @@ export default class OrderDetailsRouteComponent extends Component {
83
115
  }
84
116
 
85
117
  get hasTrackingDuration() {
86
- return this.trackerData?.route?.duration_in_traffic_s !== null && this.trackerData?.route?.duration_in_traffic_s !== undefined;
118
+ return this.showRouteEtaData && this.trackerData?.route?.duration_in_traffic_s !== null && this.trackerData?.route?.duration_in_traffic_s !== undefined;
87
119
  }
88
120
 
89
121
  get trackingDurationSeconds() {
122
+ if (!this.showRouteEtaData) {
123
+ return null;
124
+ }
125
+
90
126
  return this.trackerData?.route?.duration_in_traffic_s ?? this.trackerData?.route?.duration_s;
91
127
  }
92
128
 
@@ -150,7 +186,48 @@ export default class OrderDetailsRouteComponent extends Component {
150
186
  yield this.hostRouter.refresh();
151
187
  }
152
188
 
153
- @action async viewWaypointLabel(waypoint) {
189
+ @action changeDestination() {
190
+ this.modalsManager.show('modals/order-change-destination', {
191
+ title: 'Change destination',
192
+ order: this.args.resource,
193
+ acceptButtonText: 'Change destination',
194
+ declineButtonText: 'Close',
195
+ onChange: async () => {
196
+ if (typeof this.args.resource?.reload === 'function') {
197
+ await this.args.resource.reload();
198
+ }
199
+
200
+ if (typeof this.args.resource?.set === 'function') {
201
+ this.args.resource.set('tracker_data', null);
202
+ }
203
+
204
+ if (typeof this.args.resource?.loadTrackerData === 'function') {
205
+ await this.args.resource.loadTrackerData();
206
+ }
207
+
208
+ await this.hostRouter.refresh();
209
+ },
210
+ });
211
+ }
212
+
213
+ @action viewStopActivity(stop) {
214
+ this.modalsManager.show('modals/stop-activity', {
215
+ title: 'Stop activity',
216
+ stop,
217
+ order: this.args.resource,
218
+ hideAcceptButton: true,
219
+ declineButtonText: 'Close',
220
+ });
221
+ }
222
+
223
+ @action async viewWaypointLabel(stop) {
224
+ const waypoint = stop?.place ?? stop;
225
+ const waypointId = waypoint?.waypoint_public_id ?? stop?.waypoint_public_id;
226
+ if (!waypointId) {
227
+ this.notifications.warning('Labels are only available for waypoint stops.');
228
+ return;
229
+ }
230
+
154
231
  this.modalsManager.show(`modals/order-label`, {
155
232
  title: 'Waypoint Label',
156
233
  modalClass: 'modal-xl',
@@ -161,7 +238,7 @@ export default class OrderDetailsRouteComponent extends Component {
161
238
  try {
162
239
  // eslint-disable-next-line no-undef
163
240
  const fileReader = new FileReader();
164
- const { data: pdfStream } = await this.fetch.get(`orders/label/${waypoint.waypoint_public_id}?format=base64`);
241
+ const { data: pdfStream } = await this.fetch.get(`orders/label/${waypointId}?format=base64`);
165
242
  // eslint-disable-next-line no-undef
166
243
  const base64 = await fetch(`data:application/pdf;base64,${pdfStream}`);
167
244
  const blob = await base64.blob();
@@ -45,93 +45,122 @@
45
45
  </div>
46
46
  {{/if}}
47
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>
48
+ {{#if this.lifecycleMessage}}
49
+ <div class="tracking-intelligence-alert">
50
+ <div class="tracking-intelligence-alert__icon"><FaIcon @icon={{this.lifecycleMessageIcon}} @size="xs" /></div>
51
+ <div class="min-w-0">
52
+ <div class="tracking-intelligence-alert__title">{{this.lifecycleMessageTitle}}</div>
53
+ <div class="tracking-intelligence-alert__body">{{this.lifecycleMessage}}</div>
53
54
  </div>
54
- <div class="tracking-intelligence-cell__value">
55
- {{#if this.hasSmartAdjustedEta}}
56
- {{format-duration this.smartAdjustedEtaSeconds}}
57
- {{else}}
58
- {{this.smartAdjustedEtaUnavailableLabel}}
59
- {{/if}}
55
+ </div>
56
+ {{/if}}
57
+
58
+ {{#if this.showStartEta}}
59
+ <div class="tracking-intelligence__eta-grid">
60
+ <div class="tracking-intelligence-cell tracking-intelligence-cell--accent">
61
+ <div class="tracking-intelligence-cell__label">
62
+ Estimated start
63
+ <span class="tracking-intelligence-tag">Pre-start</span>
64
+ </div>
65
+ <div class="tracking-intelligence-cell__value">
66
+ {{#if this.hasStartEta}}
67
+ {{format-duration this.startEtaSeconds}}
68
+ {{else}}
69
+ Pending start
70
+ {{/if}}
71
+ </div>
72
+ <div class="tracking-intelligence-cell__sub">Based on the assigned driver route to the first stop</div>
60
73
  </div>
61
- <div class="tracking-intelligence-cell__sub">Based on provider route and driver signal</div>
62
74
  </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}}
75
+ {{else if this.showLiveEta}}
76
+ <div class="tracking-intelligence__eta-grid">
77
+ <div class="tracking-intelligence-cell tracking-intelligence-cell--accent">
78
+ <div class="tracking-intelligence-cell__label">
79
+ Smart adjusted ETA
80
+ <span class="tracking-intelligence-tag">Live</span>
81
+ </div>
82
+ <div class="tracking-intelligence-cell__value">
83
+ {{#if this.hasSmartAdjustedEta}}
84
+ {{format-duration this.smartAdjustedEtaSeconds}}
85
+ {{else}}
86
+ {{this.smartAdjustedEtaUnavailableLabel}}
87
+ {{/if}}
88
+ </div>
89
+ <div class="tracking-intelligence-cell__sub">Based on provider route and driver signal</div>
90
+ </div>
91
+ <div class="tracking-intelligence-cell">
92
+ <div class="tracking-intelligence-cell__label">Reported ETA</div>
93
+ <div class="tracking-intelligence-cell__value {{if this.isReportedEtaUntrusted 'tracking-intelligence-cell__value--muted'}}">
94
+ {{#if this.hasDisplayedReportedEta}}
95
+ {{format-duration this.displayedReportedEtaSeconds}}
96
+ {{else}}
97
+ {{this.etaUnavailableLabel}}
98
+ {{/if}}
99
+ </div>
100
+ <div class="tracking-intelligence-cell__sub {{if this.isReportedEtaUntrusted 'tracking-intelligence-cell__sub--warn'}}">{{this.reportedEtaWarning}}</div>
71
101
  </div>
72
- <div class="tracking-intelligence-cell__sub {{if this.isReportedEtaUntrusted 'tracking-intelligence-cell__sub--warn'}}">{{this.reportedEtaWarning}}</div>
73
102
  </div>
74
- </div>
75
103
 
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}}
104
+ <div class="tracking-intelligence-confidence">
105
+ <div class="tracking-intelligence-confidence__label">ETA confidence</div>
106
+ <div class="tracking-intelligence-confidence__segments {{this.confidenceToneClass}}">
107
+ {{#each this.confidenceSegments as |segment|}}
108
+ <span class="{{if segment.lit 'is-lit'}}"></span>
109
+ {{/each}}
110
+ </div>
111
+ <div class="tracking-intelligence-confidence__value">{{this.confidencePercent}}%</div>
112
+ </div>
113
+
114
+ <div class="tracking-intelligence-destination">
115
+ <div class="tracking-intelligence-destination__marker">{{this.activeStopMarkerLabel}}</div>
116
+ <div class="min-w-0 flex-1">
117
+ <div class="tracking-intelligence-destination__label">{{this.activeStopLabel}}</div>
118
+ <div class="tracking-intelligence-destination__address">{{n-a @resource.tracker_data.active_stop.address}}</div>
119
+ <div class="tracking-intelligence-destination__eta">
120
+ ETA:
121
+ <strong>
122
+ {{#if this.hasActiveEta}}
123
+ {{format-duration this.activeEtaSeconds}}
124
+ {{else}}
125
+ {{this.etaUnavailableLabel}}
126
+ {{/if}}
127
+ </strong>
128
+ </div>
129
+ </div>
82
130
  </div>
83
- <div class="tracking-intelligence-confidence__value">{{this.confidencePercent}}%</div>
84
- </div>
85
131
 
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:
132
+ <div class="tracking-intelligence-distance">
133
+ <div class="tracking-intelligence-distance__row">
134
+ <span>Reported total</span>
93
135
  <strong>
94
- {{#if this.hasActiveEta}}
95
- {{format-duration this.activeEtaSeconds}}
136
+ {{#if this.hasRemainingDistance}}
137
+ {{format-meters @resource.tracker_data.route.distance_m}}
96
138
  {{else}}
97
- {{this.etaUnavailableLabel}}
139
+ -
98
140
  {{/if}}
99
141
  </strong>
142
+ <div class="tracking-intelligence-distance__bar">
143
+ <span style={{this.totalProgressStyle}}></span>
144
+ </div>
100
145
  </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>
146
+ <div class="tracking-intelligence-distance__row">
147
+ <span>Between stops</span>
148
+ <strong>
149
+ {{#if this.hasCurrentLegDistance}}
150
+ {{format-meters this.currentLeg.distance_m}}
151
+ {{else}}
152
+ -
153
+ {{/if}}
154
+ </strong>
155
+ <div class="tracking-intelligence-distance__bar tracking-intelligence-distance__bar--secondary">
156
+ <span style={{this.currentLegProgressStyle}}></span>
157
+ </div>
129
158
  </div>
159
+ <div class="tracking-intelligence-distance__foot">Provider context:
160
+ {{this.providerLabel}}
161
+ route</div>
130
162
  </div>
131
- <div class="tracking-intelligence-distance__foot">Provider context:
132
- {{this.providerLabel}}
133
- route</div>
134
- </div>
163
+ {{/if}}
135
164
 
136
165
  <details class="tracking-intelligence-diagnostics">
137
166
  <summary>
@@ -34,15 +34,150 @@ export default class OrderDetailsTrackingComponent extends Component {
34
34
  return Boolean(this.trackerData);
35
35
  }
36
36
 
37
+ get lifecycle() {
38
+ return this.trackerData?.lifecycle ?? {};
39
+ }
40
+
41
+ get lifecycleMode() {
42
+ if (this.lifecycle.mode) {
43
+ return this.lifecycle.mode;
44
+ }
45
+
46
+ const status = this.statusCode;
47
+
48
+ if (['completed', 'canceled'].includes(status)) {
49
+ return status;
50
+ }
51
+
52
+ if (!this.hasAssignedDriver) {
53
+ return 'unassigned';
54
+ }
55
+
56
+ if (!this.hasOrderStarted && status === 'dispatched') {
57
+ return 'dispatched';
58
+ }
59
+
60
+ if (!this.hasOrderStarted) {
61
+ return 'pre_start';
62
+ }
63
+
64
+ return 'active';
65
+ }
66
+
67
+ get statusCode() {
68
+ return String(this.lifecycle.status ?? this.args.resource?.status ?? 'created').toLowerCase();
69
+ }
70
+
71
+ get hasOrderStarted() {
72
+ return Boolean(this.lifecycle.has_started ?? this.args.resource?.started ?? this.args.resource?.started_at ?? this.statusCode === 'started');
73
+ }
74
+
75
+ get showLiveEta() {
76
+ if (this.lifecycle.show_live_eta !== undefined) {
77
+ return Boolean(this.lifecycle.show_live_eta);
78
+ }
79
+
80
+ return this.hasOrderStarted && !this.isTerminalLifecycle;
81
+ }
82
+
83
+ get showStartEta() {
84
+ if (this.lifecycle.show_start_eta !== undefined) {
85
+ return Boolean(this.lifecycle.show_start_eta);
86
+ }
87
+
88
+ return this.lifecycleMode === 'dispatched' && this.hasStartEta;
89
+ }
90
+
91
+ get isTerminalLifecycle() {
92
+ return Boolean(this.lifecycle.is_terminal ?? ['completed', 'canceled'].includes(this.statusCode));
93
+ }
94
+
95
+ get isPreStartLifecycle() {
96
+ return ['created', 'pre_start'].includes(this.lifecycleMode);
97
+ }
98
+
99
+ get isDispatchedLifecycle() {
100
+ return this.lifecycleMode === 'dispatched';
101
+ }
102
+
103
+ get lifecycleMessage() {
104
+ if (this.lifecycleMode === 'unassigned') {
105
+ return null;
106
+ }
107
+
108
+ return this.lifecycle.message ?? this.defaultLifecycleMessage;
109
+ }
110
+
111
+ get defaultLifecycleMessage() {
112
+ if (this.statusCode === 'completed') {
113
+ return 'Order has been completed.';
114
+ }
115
+
116
+ if (this.statusCode === 'canceled') {
117
+ return 'Order has been canceled.';
118
+ }
119
+
120
+ if (this.isDispatchedLifecycle) {
121
+ return 'Order has been dispatched. Estimated start is based on the assigned driver route to the first stop.';
122
+ }
123
+
124
+ if (this.isPreStartLifecycle) {
125
+ return 'Live ETA will begin once the order is started.';
126
+ }
127
+
128
+ return null;
129
+ }
130
+
131
+ get lifecycleMessageTitle() {
132
+ switch (this.lifecycleMode) {
133
+ case 'completed':
134
+ return 'Order completed';
135
+ case 'canceled':
136
+ return 'Order canceled';
137
+ case 'dispatched':
138
+ return 'Order dispatched';
139
+ case 'pre_start':
140
+ case 'created':
141
+ return 'Tracking pending start';
142
+ default:
143
+ return null;
144
+ }
145
+ }
146
+
147
+ get lifecycleMessageIcon() {
148
+ switch (this.lifecycleMode) {
149
+ case 'completed':
150
+ return 'circle-check';
151
+ case 'canceled':
152
+ return 'ban';
153
+ case 'dispatched':
154
+ return 'route';
155
+ default:
156
+ return 'clock';
157
+ }
158
+ }
159
+
160
+ get startEtaSeconds() {
161
+ return this.trackerData?.eta?.start_seconds;
162
+ }
163
+
164
+ get hasStartEta() {
165
+ return this.startEtaSeconds !== null && this.startEtaSeconds !== undefined;
166
+ }
167
+
37
168
  get activeEtaSeconds() {
38
169
  return this.trackerData?.eta?.active_stop_seconds;
39
170
  }
40
171
 
41
172
  get hasActiveEta() {
42
- return this.activeEtaSeconds !== null && this.activeEtaSeconds !== undefined;
173
+ return this.showLiveEta && this.activeEtaSeconds !== null && this.activeEtaSeconds !== undefined;
43
174
  }
44
175
 
45
176
  get smartAdjustedEtaSeconds() {
177
+ if (!this.showLiveEta) {
178
+ return null;
179
+ }
180
+
46
181
  return (
47
182
  this.firstPositiveNumber(this.activeEtaSeconds) ??
48
183
  this.firstPositiveNumber(this.trackerData?.route?.duration_in_traffic_s) ??
@@ -69,7 +204,7 @@ export default class OrderDetailsTrackingComponent extends Component {
69
204
  }
70
205
 
71
206
  get hasCompletionEta() {
72
- return Boolean(this.trackerData?.eta?.completion_at);
207
+ return this.showLiveEta && Boolean(this.trackerData?.eta?.completion_at);
73
208
  }
74
209
 
75
210
  get hasRemainingDistance() {
@@ -249,6 +384,10 @@ export default class OrderDetailsTrackingComponent extends Component {
249
384
  }
250
385
 
251
386
  get displayedReportedEtaSeconds() {
387
+ if (!this.showLiveEta) {
388
+ return null;
389
+ }
390
+
252
391
  return (
253
392
  this.firstPositiveNumber(this.reportedEtaSeconds) ??
254
393
  this.firstPositiveNumber(this.activeEtaSeconds) ??
@@ -269,6 +408,10 @@ export default class OrderDetailsTrackingComponent extends Component {
269
408
  return false;
270
409
  }
271
410
 
411
+ if (!this.showLiveEta) {
412
+ return false;
413
+ }
414
+
272
415
  return !trackerData?.driver?.location || trackerData?.insights?.is_location_stale || trackerData.fallback_provider || (trackerData.confidence && trackerData.confidence !== 'high');
273
416
  }
274
417
 
@@ -402,6 +545,10 @@ export default class OrderDetailsTrackingComponent extends Component {
402
545
  return null;
403
546
  }
404
547
 
548
+ if (this.isTerminalLifecycle || this.isPreStartLifecycle || this.isDispatchedLifecycle) {
549
+ return null;
550
+ }
551
+
405
552
  if (!this.hasAssignedDriver) {
406
553
  return 'Assign a driver to start live tracking and improve ETA accuracy.';
407
554
  }
@@ -1,6 +1,6 @@
1
1
  <ContentPanel @title="Details" @open={{true}} @wrapperClass="bordered-top">
2
2
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
3
- <InputGroup @name={{t "order.fields.order-type"}} @helpText={{t "order.fields.order-type-help-text"}}>
3
+ <InputGroup @name={{t "order.fields.order-type"}} @helpText={{t "order.fields.order-type-help-text"}} @required={{true}}>
4
4
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
5
5
  <PowerSelect
6
6
  @options={{this.orderConfigActions.allOrderConfigs}}
@@ -160,7 +160,7 @@
160
160
  </InputGroup>
161
161
 
162
162
  {{#if (and @resource.pod_required (not @resource.facilitator.isIntegratedVendor))}}
163
- <InputGroup @name={{t "order.fields.proof-delivery"}} @helpText={{t "order.fields.proof-delivery-help-text"}}>
163
+ <InputGroup @name={{t "order.fields.proof-delivery"}} @helpText={{t "order.fields.proof-delivery-help-text"}} @required={{true}}>
164
164
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
165
165
  <PowerSelect
166
166
  @options={{get-fleet-ops-options "podOptions"}}
@@ -279,4 +279,4 @@
279
279
  </div>
280
280
  </ContentPanel>
281
281
 
282
- <Order::Form::CustomFields @resource={{@resource}} @customFields={{this.customFields}} />
282
+ <Order::Form::CustomFields @resource={{@resource}} @customFields={{this.customFields}} />
@@ -35,12 +35,19 @@ export default class OrderFormDocumentsComponent extends Component {
35
35
 
36
36
  try {
37
37
  this.uploadQueue.pushObject(file);
38
+ const uploadOptions = {
39
+ path: 'uploads/fleet-ops/order-files',
40
+ type: 'order_file',
41
+ };
42
+
43
+ if (!this.args.resource.isNew) {
44
+ uploadOptions.subject_uuid = this.args.resource.id;
45
+ uploadOptions.subject_type = 'fleet-ops:order';
46
+ }
47
+
38
48
  yield this.fetch.uploadFile.perform(
39
49
  file,
40
- {
41
- path: 'uploads/fleet-ops/order-files',
42
- type: 'order_file',
43
- },
50
+ uploadOptions,
44
51
  (uploadedFile) => {
45
52
  this.args.resource.files.pushObject(uploadedFile);
46
53
  this.uploadQueue.removeObject(file);