@fleetbase/fleetops-engine 0.6.48 → 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 (266) 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/driver/details.hbs +5 -1
  7. package/addon/components/driver/form.hbs +11 -1
  8. package/addon/components/fleet-listing-panel.hbs +2 -2
  9. package/addon/components/fleet-listing-panel.js +10 -0
  10. package/addon/components/map/google-live-map.js +18 -75
  11. package/addon/components/map/leaflet-live-map.hbs +187 -37
  12. package/addon/components/map/leaflet-live-map.js +184 -20
  13. package/addon/components/modals/order-change-destination.hbs +46 -0
  14. package/addon/components/modals/order-change-destination.js +106 -0
  15. package/addon/components/modals/stop-activity.hbs +28 -0
  16. package/addon/components/modals/stop-activity.js +52 -0
  17. package/addon/components/modals/update-order-activity.hbs +45 -3
  18. package/addon/components/modals/update-order-activity.js +143 -5
  19. package/addon/components/orchestrator/phase-builder.hbs +21 -0
  20. package/addon/components/orchestrator/phase-builder.js +13 -0
  21. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  22. package/addon/components/orchestrator/resource-panel.js +2 -4
  23. package/addon/components/orchestrator-workbench.js +1 -0
  24. package/addon/components/order/activity-list.hbs +2 -2
  25. package/addon/components/order/activity-timeline.hbs +31 -29
  26. package/addon/components/order/details/activity.hbs +3 -3
  27. package/addon/components/order/details/activity.js +11 -0
  28. package/addon/components/order/details/documents.js +2 -0
  29. package/addon/components/order/details/purchase-rate.hbs +2 -2
  30. package/addon/components/order/details/route.hbs +11 -3
  31. package/addon/components/order/details/route.js +91 -14
  32. package/addon/components/order/details/tracking.hbs +101 -72
  33. package/addon/components/order/details/tracking.js +149 -2
  34. package/addon/components/order/form/details.hbs +3 -3
  35. package/addon/components/order/form/documents.js +11 -4
  36. package/addon/components/order/form/route.hbs +108 -104
  37. package/addon/components/order/form/route.js +37 -1
  38. package/addon/components/order/form/service-rate.hbs +2 -2
  39. package/addon/components/order/form.hbs +2 -2
  40. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  41. package/addon/components/order-config-manager/activity-flow.js +178 -12
  42. package/addon/components/place/address.js +1 -1
  43. package/addon/components/route-list.hbs +17 -17
  44. package/addon/components/route-list.js +42 -10
  45. package/addon/components/service-rate/details.hbs +41 -0
  46. package/addon/components/service-rate/form.hbs +118 -0
  47. package/addon/components/service-rate/form.js +55 -0
  48. package/addon/components/vehicle/form.js +1 -1
  49. package/addon/components/vehicle/pill.hbs +1 -1
  50. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  51. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  52. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  53. package/addon/components/widget/fuel-efficiency.js +43 -0
  54. package/addon/components/widget/geofence-violations.hbs +44 -0
  55. package/addon/components/widget/geofence-violations.js +55 -0
  56. package/addon/components/widget/issues-insights.hbs +43 -0
  57. package/addon/components/widget/issues-insights.js +62 -0
  58. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  59. package/addon/components/widget/kpi-active-orders.js +5 -0
  60. package/addon/components/widget/kpi-aov.hbs +10 -0
  61. package/addon/components/widget/kpi-aov.js +5 -0
  62. package/addon/components/widget/kpi-distance.hbs +10 -0
  63. package/addon/components/widget/kpi-distance.js +5 -0
  64. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  65. package/addon/components/widget/kpi-drivers-online.js +5 -0
  66. package/addon/components/widget/kpi-earnings.hbs +10 -0
  67. package/addon/components/widget/kpi-earnings.js +5 -0
  68. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  69. package/addon/components/widget/kpi-open-issues.js +5 -0
  70. package/addon/components/widget/kpi-tile.hbs +59 -0
  71. package/addon/components/widget/kpi-tile.js +168 -0
  72. package/addon/components/widget/live-fleet.hbs +122 -0
  73. package/addon/components/widget/live-fleet.js +365 -0
  74. package/addon/components/widget/maintenance-overview.hbs +45 -0
  75. package/addon/components/widget/maintenance-overview.js +40 -0
  76. package/addon/components/widget/on-time-delivery.hbs +27 -0
  77. package/addon/components/widget/on-time-delivery.js +73 -0
  78. package/addon/components/widget/operations-pulse.hbs +51 -0
  79. package/addon/components/widget/operations-pulse.js +173 -0
  80. package/addon/components/widget/orders-by-status.hbs +26 -0
  81. package/addon/components/widget/orders-by-status.js +54 -0
  82. package/addon/components/widget/revenue-trend.hbs +34 -0
  83. package/addon/components/widget/revenue-trend.js +67 -0
  84. package/addon/components/widget/top-drivers.hbs +44 -0
  85. package/addon/components/widget/top-drivers.js +54 -0
  86. package/addon/controllers/management/vehicles/index/new.js +1 -1
  87. package/addon/controllers/operations/orders/index/details.js +4 -4
  88. package/addon/controllers/operations/orders/index.js +14 -0
  89. package/addon/controllers/settings/routing.js +26 -0
  90. package/addon/extension.js +172 -3
  91. package/addon/helpers/place-address-html.js +2 -2
  92. package/addon/routes/operations/service-rates/index.js +1 -1
  93. package/addon/services/geofence.js +163 -19
  94. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  95. package/addon/services/map-adapter/google.js +13 -2
  96. package/addon/services/vehicle-actions.js +1 -1
  97. package/addon/styles/fleetops-engine.css +205 -88
  98. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  99. package/addon/templates/maintenance/parts/index.hbs +1 -1
  100. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  101. package/addon/templates/management/vehicles/index.hbs +1 -1
  102. package/addon/templates/settings/avatars.hbs +1 -1
  103. package/addon/templates/settings/custom-fields.hbs +1 -1
  104. package/addon/templates/settings/map.hbs +1 -1
  105. package/addon/templates/settings/navigator-app.hbs +1 -1
  106. package/addon/templates/settings/notifications.hbs +1 -1
  107. package/addon/templates/settings/orchestrator.hbs +1 -1
  108. package/addon/templates/settings/payments/index.hbs +1 -1
  109. package/addon/templates/settings/payments/onboard.hbs +1 -1
  110. package/addon/templates/settings/routing.hbs +22 -2
  111. package/addon/templates/settings/scheduling.hbs +1 -1
  112. package/addon/utils/fleet-ops-options.js +2 -1
  113. package/addon/utils/live-map-card-content.js +171 -0
  114. package/addon/utils/normalize-order-config-flow.js +41 -0
  115. package/addon/utils/place-address-html.js +3 -2
  116. package/addon/utils/route-visualization.js +54 -7
  117. package/app/components/modals/order-change-destination.js +1 -0
  118. package/app/components/modals/stop-activity.js +1 -0
  119. package/app/components/widget/fuel-efficiency.js +1 -0
  120. package/app/components/widget/geofence-violations.js +1 -0
  121. package/app/components/widget/issues-insights.js +1 -0
  122. package/app/components/widget/kpi-active-orders.js +1 -0
  123. package/app/components/widget/kpi-aov.js +1 -0
  124. package/app/components/widget/kpi-distance.js +1 -0
  125. package/app/components/widget/kpi-drivers-online.js +1 -0
  126. package/app/components/widget/kpi-earnings.js +1 -0
  127. package/app/components/widget/kpi-open-issues.js +1 -0
  128. package/app/components/widget/kpi-tile.js +1 -0
  129. package/app/components/widget/live-fleet.js +1 -0
  130. package/app/components/widget/maintenance-overview.js +1 -0
  131. package/app/components/widget/on-time-delivery.js +1 -0
  132. package/app/components/widget/operations-pulse.js +1 -0
  133. package/app/components/widget/orders-by-status.js +1 -0
  134. package/app/components/widget/revenue-trend.js +1 -0
  135. package/app/components/widget/top-drivers.js +1 -0
  136. package/app/utils/live-map-card-content.js +1 -0
  137. package/composer.json +1 -1
  138. package/extension.json +1 -1
  139. package/package.json +3 -3
  140. package/pnpm-workspace.yaml +1 -0
  141. package/server/config/telematics.php +111 -1
  142. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  143. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  144. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  145. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  146. package/server/resources/views/labels/default.php +57 -49
  147. package/server/resources/views/labels/waypoint-label.php +27 -20
  148. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  149. package/server/seeders/Testing/ConnectivitySeeder.php +213 -0
  150. package/server/seeders/Testing/FleetSeeder.php +341 -0
  151. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  152. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  153. package/server/seeders/Testing/OrdersSeeder.php +198 -0
  154. package/server/seeders/Testing/TestingSeeder.php +40 -0
  155. package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +29 -22
  156. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  157. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  158. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  159. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  160. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  161. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  162. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  163. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  164. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  165. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  166. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  167. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  168. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  169. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  170. package/server/src/Http/Controllers/Internal/v1/OrderController.php +441 -32
  171. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  172. package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
  173. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  174. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  175. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  176. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  177. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  178. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  179. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  180. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  181. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  182. package/server/src/Http/Resources/v1/Customer.php +79 -0
  183. package/server/src/Http/Resources/v1/Driver.php +2 -0
  184. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  185. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  186. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  187. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  188. package/server/src/Http/Resources/v1/Payload.php +23 -20
  189. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  190. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  191. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  192. package/server/src/Models/Contact.php +92 -15
  193. package/server/src/Models/Customer.php +64 -0
  194. package/server/src/Models/Driver.php +14 -3
  195. package/server/src/Models/Entity.php +2 -2
  196. package/server/src/Models/Order.php +29 -4
  197. package/server/src/Models/OrderConfig.php +14 -11
  198. package/server/src/Models/Payload.php +17 -1
  199. package/server/src/Models/ServiceRate.php +273 -1
  200. package/server/src/Models/ServiceRateFee.php +39 -9
  201. package/server/src/Models/TrackingNumber.php +8 -2
  202. package/server/src/Models/Vehicle.php +18 -2
  203. package/server/src/Models/Waypoint.php +2 -2
  204. package/server/src/Notifications/LateDeparture.php +48 -0
  205. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  206. package/server/src/Notifications/RouteDeviation.php +48 -0
  207. package/server/src/Observers/ContactObserver.php +2 -0
  208. package/server/src/Observers/ServiceRateObserver.php +6 -6
  209. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +29 -2
  210. package/server/src/Providers/FleetOpsServiceProvider.php +6 -0
  211. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  212. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  213. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  214. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  215. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  216. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  217. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  218. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  219. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  220. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  221. package/server/src/Support/Analytics/RevenueTrend.php +78 -0
  222. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  223. package/server/src/Support/CustomerAuth.php +98 -0
  224. package/server/src/Support/LabelPdf.php +52 -0
  225. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  226. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  227. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  228. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  229. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  230. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  231. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  232. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  233. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  234. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  235. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  236. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  237. package/server/src/Support/Metrics/Registry.php +46 -0
  238. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  239. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  240. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  241. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  242. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  243. package/server/src/Support/Metrics.php +46 -249
  244. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  245. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  246. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  247. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  248. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  249. package/server/src/Tracking/TrackingStop.php +13 -11
  250. package/server/src/routes.php +50 -0
  251. package/server/tests/AnalyticsRoutesTest.php +41 -0
  252. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  253. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  254. package/server/tests/CustomerEndpointTest.php +199 -0
  255. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  256. package/server/tests/LabelPdfRenderingTest.php +23 -0
  257. package/server/tests/LiveControllerViewportTest.php +60 -0
  258. package/server/tests/MetricsRegistryTest.php +84 -0
  259. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  260. package/server/tests/OrderConfigEndpointTest.php +86 -0
  261. package/server/tests/OrderFileAttachmentTest.php +20 -0
  262. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  263. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  264. package/server/tests/VroomOrchestrationEngineTest.php +91 -0
  265. package/translations/en-us.yaml +38 -0
  266. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -1,16 +1,21 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
+ import { action, get } from '@ember/object';
4
5
  import { task } from 'ember-concurrency';
6
+ import config from 'ember-get-config';
7
+ import { buildServiceStopsFromPayload } from '../../utils/route-visualization';
5
8
 
6
9
  export default class ModalsUpdateOrderActivityComponent extends Component {
7
10
  @service fetch;
11
+ @service intl;
8
12
  @service notifications;
9
13
  @service modalsManager;
10
14
  @tracked activityOptions = [];
11
15
  @tracked order;
12
16
  @tracked selectedActivity;
13
- @tracked customActivity;
17
+ @tracked customActivity = {};
18
+ @tracked proofFiles = [];
14
19
 
15
20
  constructor(owner, { options = {} }) {
16
21
  super(...arguments);
@@ -18,9 +23,95 @@ export default class ModalsUpdateOrderActivityComponent extends Component {
18
23
  this.loadActivity.perform();
19
24
  }
20
25
 
26
+ get waypoints() {
27
+ const waypoints = this.order?.payload?.waypoints;
28
+ if (typeof waypoints?.toArray === 'function') {
29
+ return waypoints.toArray();
30
+ }
31
+
32
+ return Array.isArray(waypoints) ? waypoints : Array.from(waypoints ?? []);
33
+ }
34
+
35
+ get serviceStops() {
36
+ return buildServiceStopsFromPayload(this.order?.payload);
37
+ }
38
+
39
+ get activeStopId() {
40
+ const activeStop = this.order?.tracker_data?.active_stop;
41
+
42
+ return activeStop?.uuid ?? activeStop?.public_id ?? this.order?.payload?.current_waypoint_uuid ?? this.order?.payload?.current_waypoint;
43
+ }
44
+
45
+ get isMultipleWaypointOrder() {
46
+ return this.waypoints.length > 0;
47
+ }
48
+
49
+ get orderHasStarted() {
50
+ const status = this.order?.status;
51
+
52
+ return Boolean(this.order?.started || this.order?.started_at || (status && !['created', 'dispatched'].includes(status)));
53
+ }
54
+
55
+ get shouldUseWaypointActivity() {
56
+ return this.isMultipleWaypointOrder && this.orderHasStarted;
57
+ }
58
+
59
+ get currentWaypoint() {
60
+ if (!this.shouldUseWaypointActivity) {
61
+ return null;
62
+ }
63
+
64
+ const currentWaypointId = this.activeStopId;
65
+
66
+ if (currentWaypointId) {
67
+ const stop = this.serviceStops.find(({ place }) => {
68
+ return [place?.id, place?.uuid, place?.public_id, place?.waypoint_public_id].includes(currentWaypointId);
69
+ });
70
+
71
+ if (stop?.place) {
72
+ return stop.place;
73
+ }
74
+ }
75
+
76
+ return this.serviceStops.find(({ place }) => !place?.complete)?.place ?? this.serviceStops[0]?.place;
77
+ }
78
+
79
+ get currentWaypointId() {
80
+ const waypoint = this.currentWaypoint;
81
+
82
+ return waypoint?.id ?? waypoint?.public_id ?? waypoint?.uuid;
83
+ }
84
+
85
+ get currentWaypointProofSubjectId() {
86
+ const waypoint = this.currentWaypoint;
87
+
88
+ return waypoint?.waypoint_public_id ?? waypoint?.id ?? waypoint?.public_id ?? waypoint?.uuid;
89
+ }
90
+
91
+ get selectedActivityRecord() {
92
+ if (this.selectedActivity === 'custom') {
93
+ return this.customActivity;
94
+ }
95
+
96
+ return this.activityOptions[this.selectedActivity];
97
+ }
98
+
99
+ get selectedActivityRequiresPod() {
100
+ return Boolean(this.selectedActivityRecord?.require_pod);
101
+ }
102
+
103
+ get acceptedProofFileTypes() {
104
+ return ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
105
+ }
106
+
21
107
  @task *loadActivity() {
108
+ if (!this.order?.id) {
109
+ return [];
110
+ }
111
+
22
112
  try {
23
- const activityOptions = yield this.fetch.get(`orders/next-activity/${this.order.id}`);
113
+ const params = this.shouldUseWaypointActivity && this.currentWaypointId ? { waypoint: this.currentWaypointId } : {};
114
+ const activityOptions = yield this.fetch.get(`orders/next-activity/${this.order.id}`, params);
24
115
  this.activityOptions = activityOptions;
25
116
  return activityOptions;
26
117
  } catch (err) {
@@ -31,21 +122,68 @@ export default class ModalsUpdateOrderActivityComponent extends Component {
31
122
  @task *submitActivity(modal) {
32
123
  modal.startLoading();
33
124
 
34
- const activity = this.selectedActivity === 'custom' ? this.customActivity : this.activityOptions[this.selectedActivity];
125
+ const activity = this.selectedActivityRecord;
35
126
  if (this.selectedActivity === 'custom' && !activity.status && !activity.details && !activity.code) {
36
127
  modal.stopLoading();
37
128
  return this.notifications.warning(this.intl.t('fleet-ops.operations.orders.index.view.invalid-warning'));
38
129
  }
39
130
 
131
+ if (this.selectedActivityRequiresPod && this.proofFiles.length === 0) {
132
+ modal.stopLoading();
133
+ return this.notifications.warning('Photo proof of delivery is required for this activity.');
134
+ }
135
+
40
136
  try {
41
- yield this.fetch.patch(`orders/update-activity/${this.order.id}`, { activity });
137
+ const proof = this.selectedActivityRequiresPod ? yield this.capturePhotoProof.perform() : null;
138
+ yield this.fetch.patch(`orders/update-activity/${this.order.id}`, { activity, proof: proof?.id ?? proof?.public_id });
139
+
140
+ if (typeof this.order?.reload === 'function') {
141
+ yield this.order.reload();
142
+ }
143
+
144
+ if (typeof this.order?.loadTrackerData === 'function') {
145
+ if (typeof this.order.set === 'function') {
146
+ this.order.set('tracker_data', null);
147
+ }
148
+
149
+ yield this.order.loadTrackerData();
150
+ }
151
+
42
152
  this.modalsManager.setOption('activityCreated', activity);
43
153
  this.notifications.success(`Order activity has been updated to ${activity.status}`);
154
+ modal.done();
44
155
  } catch (error) {
45
156
  this.notifications.serverError(error);
46
157
  } finally {
47
158
  modal.stopLoading();
48
- modal.done();
49
159
  }
50
160
  }
161
+
162
+ @task *capturePhotoProof() {
163
+ const [proofFile] = this.proofFiles;
164
+ const headers = this.fetch.getHeaders();
165
+ delete headers['Content-Type'];
166
+
167
+ const subjectId = this.currentWaypointProofSubjectId;
168
+ const subjectPath = subjectId ? `/${subjectId}` : '';
169
+ const url = [get(config, 'API.host'), get(config, 'API.namespace'), `orders/${this.order.id}/capture-photo${subjectPath}`].filter(Boolean).join('/');
170
+
171
+ const response = yield proofFile.upload(url, {
172
+ data: {
173
+ remarks: 'Verified by Photo',
174
+ },
175
+ headers,
176
+ withCredentials: true,
177
+ });
178
+
179
+ return response.json();
180
+ }
181
+
182
+ @action setProofFile(file) {
183
+ if (!file) {
184
+ return;
185
+ }
186
+
187
+ this.proofFiles = [file];
188
+ }
51
189
  }
@@ -130,6 +130,27 @@
130
130
  </InputGroup>
131
131
  {{/if}}
132
132
 
133
+ {{#if this.shouldShowVehiclePacking}}
134
+ <InputGroup @name={{t "orchestrator.vehicle-packing"}}>
135
+ <div class="grid grid-cols-2 gap-1">
136
+ {{#each this.vehiclePackingOptions as |opt|}}
137
+ <button
138
+ type="button"
139
+ class="flex items-center justify-center gap-1.5 px-2 py-1.5 rounded border text-xs transition-colors
140
+ {{if
141
+ (eq this.draftPhase.vehiclePacking opt.value)
142
+ 'bg-indigo-50 dark:bg-indigo-900/30 border-indigo-400 dark:border-indigo-600 text-indigo-700 dark:text-indigo-300'
143
+ 'border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-gray-300 dark:hover:border-gray-500'
144
+ }}"
145
+ {{on "click" (fn this.setDraftOption "vehiclePacking" opt.value)}}
146
+ >
147
+ {{opt.label}}
148
+ </button>
149
+ {{/each}}
150
+ </div>
151
+ </InputGroup>
152
+ {{/if}}
153
+
133
154
  {{! Constraint toggles }}
134
155
  <InputGroup @name={{t "orchestrator.constraints"}}>
135
156
  <div class="space-y-1.5">
@@ -51,6 +51,7 @@ export default class OrchestratorPhaseBuilderComponent extends Component {
51
51
  label: this.intl.t(`orchestrator.mode-${mode.replace(/_/g, '-')}`),
52
52
  engine: 'greedy',
53
53
  allocationStrategy: 'route_aware',
54
+ vehiclePacking: 'minimize_vehicles',
54
55
  orderStatuses: ['created'],
55
56
  balanceWorkload: false,
56
57
  respectSkills: true,
@@ -130,6 +131,7 @@ export default class OrchestratorPhaseBuilderComponent extends Component {
130
131
  ...this.draftPhase,
131
132
  engine,
132
133
  allocationStrategy: engine === 'capacity' ? 'capacity_only' : (this.draftPhase.allocationStrategy ?? 'route_aware'),
134
+ vehiclePacking: this.draftPhase.vehiclePacking ?? 'minimize_vehicles',
133
135
  };
134
136
  }
135
137
 
@@ -158,10 +160,21 @@ export default class OrchestratorPhaseBuilderComponent extends Component {
158
160
  ];
159
161
  }
160
162
 
163
+ get vehiclePackingOptions() {
164
+ return [
165
+ { value: 'minimize_vehicles', label: this.intl.t('orchestrator.vehicle-packing-minimize-vehicles') },
166
+ { value: 'balanced', label: this.intl.t('orchestrator.vehicle-packing-balanced') },
167
+ ];
168
+ }
169
+
161
170
  get shouldShowAllocationStrategy() {
162
171
  return this.draftPhase?.mode === 'assign_vehicles' && this.draftPhase?.engine === 'vroom';
163
172
  }
164
173
 
174
+ get shouldShowVehiclePacking() {
175
+ return this.shouldShowAllocationStrategy && this.draftPhase?.allocationStrategy === 'capacity_only';
176
+ }
177
+
165
178
  get shouldShowRouteOptions() {
166
179
  return this.draftPhase?.engine !== 'capacity' && this.draftPhase?.allocationStrategy !== 'capacity_only';
167
180
  }
@@ -69,16 +69,6 @@
69
69
  }}"
70
70
  {{on "click" (fn this.setVehicleFilter "all")}}
71
71
  >{{t "orchestrator.filter-all"}}</button>
72
- <button
73
- type="button"
74
- class="px-2 py-0.5 rounded text-xs transition-colors
75
- {{if
76
- (eq this.vehicleFilter 'active')
77
- 'bg-indigo-500 text-white'
78
- 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
79
- }}"
80
- {{on "click" (fn this.setVehicleFilter "active")}}
81
- >{{t "orchestrator.filter-active"}}</button>
82
72
  <button
83
73
  type="button"
84
74
  class="px-2 py-0.5 rounded text-xs transition-colors
@@ -59,12 +59,10 @@ export default class OrchestratorResourcePanelComponent extends Component {
59
59
  );
60
60
  }
61
61
 
62
- if (this.vehicleFilter === 'active') {
63
- vehicles = vehicles.filter((v) => v.status === 'active');
64
- } else if (this.vehicleFilter === 'no-driver') {
62
+ if (this.vehicleFilter === 'no-driver') {
65
63
  vehicles = vehicles.filter((v) => !v.driver?.id);
66
64
  } else if (this.vehicleFilter === 'available') {
67
- vehicles = vehicles.filter((v) => v.status === 'active' && !v.driver?.current_job);
65
+ vehicles = vehicles.filter((v) => v.status === 'available' && !v.driver?.current_job);
68
66
  }
69
67
 
70
68
  return vehicles;
@@ -271,6 +271,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
271
271
  options: {
272
272
  engine: phase.engine ?? 'greedy',
273
273
  allocation_strategy: phase.allocationStrategy ?? 'route_aware',
274
+ vehicle_packing: phase.vehiclePacking ?? 'minimize_vehicles',
274
275
  balance_workload: phase.balanceWorkload ?? false,
275
276
  respect_skills: phase.respectSkills ?? true,
276
277
  respect_capacity: phase.respectCapacity ?? true,
@@ -1,5 +1,5 @@
1
1
  <div class="order-activity-list space-y-2">
2
- {{#each @resource.tracking_statuses as |trackingStatus|}}
2
+ {{#each (or @activity @resource.tracking_statuses) as |trackingStatus|}}
3
3
  <div class="order-activity-list-item flex flex-row justify-between">
4
4
  <div class="flex flex-row">
5
5
  <div class="w-8 pt-1">
@@ -31,4 +31,4 @@
31
31
  </div>
32
32
  </div>
33
33
  {{/each}}
34
- </div>
34
+ </div>
@@ -1,33 +1,35 @@
1
- {{#if @resource.tracking_statuses}}
2
- <Timeline @activity={{@resource.tracking_statuses}} @arrowClass="bg-gray-600 border border-gray-900 shadow-md" as |TimelineItem|>
3
- <TimelineItem @activeStatus={{@resource.status}} as |trackingStatus|>
4
- <div class="flex flex-col px-3 py-2 rounded text-sm leading-4 space-y-2 bg-gray-100 dark:bg-gray-900 dark:text-white">
5
- <div class="flex flex-col space-y-1">
6
- <div class="text-xs font-semibold truncate">{{trackingStatus.status}}</div>
7
- <div class="text-xs truncate">{{n-a trackingStatus.details}}</div>
8
- <div class="text-xs">{{format-date-fns trackingStatus.created_at "d MMM HH:mm"}}</div>
1
+ {{#let (or @activity @resource.tracking_statuses) as |activity|}}
2
+ {{#if activity.length}}
3
+ <Timeline @activity={{activity}} @arrowClass="bg-gray-600 border border-gray-900 shadow-md" as |TimelineItem|>
4
+ <TimelineItem @activeStatus={{@resource.status}} as |trackingStatus|>
5
+ <div class="flex flex-col px-3 py-2 rounded text-sm leading-4 space-y-2 bg-gray-100 dark:bg-gray-900 dark:text-white">
6
+ <div class="flex flex-col space-y-1">
7
+ <div class="text-xs font-semibold truncate">{{trackingStatus.status}}</div>
8
+ <div class="text-xs truncate">{{n-a trackingStatus.details}}</div>
9
+ <div class="text-xs">{{format-date-fns trackingStatus.created_at "d MMM HH:mm"}}</div>
10
+ </div>
11
+ <Attach::Tooltip @class="clean" @animation="scale" @placement="top">
12
+ <InputInfo>
13
+ <div class="text-xs">{{or trackingStatus.details trackingStatus.status}}</div>
14
+ <div class="text-xs">{{format-date-fns trackingStatus.created_at "d MMM yyyy HH:mm"}}</div>
15
+ </InputInfo>
16
+ </Attach::Tooltip>
9
17
  </div>
10
- <Attach::Tooltip @class="clean" @animation="scale" @placement="top">
11
- <InputInfo>
12
- <div class="text-xs">{{or trackingStatus.details trackingStatus.status}}</div>
13
- <div class="text-xs">{{format-date-fns trackingStatus.created_at "d MMM yyyy HH:mm"}}</div>
14
- </InputInfo>
15
- </Attach::Tooltip>
16
- </div>
17
- </TimelineItem>
18
- </Timeline>
19
- {{else}}
20
- <div class="min-h-24 p-4">
21
- <div class="flex items-center justify-center border border-dashed border-gray-300 dark:border-gray-600 p-6 rounded-xl bg-gray-50 dark:bg-gray-800">
22
- <div class="text-center">
23
- <FaIcon @icon="layer-group" @size="2x" class="text-gray-400 mb-2" />
24
- <div class="text-sm text-gray-600 dark:text-gray-400 font-medium">
25
- No order activity
26
- </div>
27
- <div class="text-xs text-gray-500 dark:text-gray-500 mt-1">
28
- Dispatch or update the order to create activity
18
+ </TimelineItem>
19
+ </Timeline>
20
+ {{else}}
21
+ <div class="min-h-24 p-4">
22
+ <div class="flex items-center justify-center border border-dashed border-gray-300 dark:border-gray-600 p-6 rounded-xl bg-gray-50 dark:bg-gray-800">
23
+ <div class="text-center">
24
+ <FaIcon @icon="layer-group" @size="2x" class="text-gray-400 mb-2" />
25
+ <div class="text-sm text-gray-600 dark:text-gray-400 font-medium">
26
+ No order activity
27
+ </div>
28
+ <div class="text-xs text-gray-500 dark:text-gray-500 mt-1">
29
+ Dispatch or update the order to create activity
30
+ </div>
29
31
  </div>
30
32
  </div>
31
33
  </div>
32
- </div>
33
- {{/if}}
34
+ {{/if}}
35
+ {{/let}}
@@ -6,8 +6,8 @@
6
6
  @wrapperClass="bordered-top"
7
7
  >
8
8
  {{#if (eq this.layout "timeline")}}
9
- <Order::ActivityTimeline @resource={{@resource}} />
9
+ <Order::ActivityTimeline @resource={{@resource}} @activity={{this.parentOrderActivity}} />
10
10
  {{else}}
11
- <Order::ActivityList @resource={{@resource}} />
11
+ <Order::ActivityList @resource={{@resource}} @activity={{this.parentOrderActivity}} />
12
12
  {{/if}}
13
- </ContentPanel>
13
+ </ContentPanel>
@@ -9,6 +9,17 @@ export default class OrderDetailsActivityComponent extends Component {
9
9
  @service intl;
10
10
  @tracked layout = this.appCache.get('fleetops:order:activity:layout', 'timeline');
11
11
 
12
+ get parentOrderActivity() {
13
+ const orderTrackingNumberUuid = this.args.resource?.tracking_number_uuid;
14
+ const activity = this.args.resource?.tracking_statuses ?? [];
15
+
16
+ if (!orderTrackingNumberUuid) {
17
+ return activity;
18
+ }
19
+
20
+ return activity.filter((trackingStatus) => trackingStatus.tracking_number_uuid === orderTrackingNumberUuid);
21
+ }
22
+
12
23
  /* eslint-disable ember/no-side-effects */
13
24
  get actionButtons() {
14
25
  return [
@@ -39,6 +39,8 @@ export default class OrderDetailsDocumentsComponent extends Component {
39
39
  file,
40
40
  {
41
41
  path: 'uploads/fleet-ops/order-files',
42
+ subject_uuid: this.args.resource.id,
43
+ subject_type: 'fleet-ops:order',
42
44
  type: 'order_file',
43
45
  },
44
46
  (uploadedFile) => {
@@ -12,7 +12,7 @@
12
12
  {{#each @resource.purchase_rate.service_quote.items as |item|}}
13
13
  <tr class="border-b border-gray-100 dark:border-gray-800 last:border-0">
14
14
  <td class="py-1 pr-2 text-gray-700 dark:text-gray-300">
15
- {{smart-humanize item.details}}
15
+ {{item.details}}
16
16
  </td>
17
17
  <td class="py-1 pl-2 text-right font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">
18
18
  {{format-currency item.amount @resource.purchase_rate.service_quote.currency}}
@@ -33,4 +33,4 @@
33
33
  </table>
34
34
  </div>
35
35
  </ContentPanel>
36
- {{/if}}
36
+ {{/if}}
@@ -26,17 +26,25 @@
26
26
 
27
27
  {{#if @resource.hasIntermediateWaypoints}}
28
28
  <div>
29
- <RouteList @order={{@resource}} @eta={{@resource.eta}} @waypointActions={{hash viewWaypointLabel=(hash label=(t "order.fields.get-label") fn=this.viewWaypointLabel)}} />
29
+ <RouteList
30
+ @order={{@resource}}
31
+ @eta={{if this.showRouteEtaData @resource.eta}}
32
+ @showEta={{this.showRouteEtaData}}
33
+ @waypointActions={{hash
34
+ viewStopActivity=(hash label="View activity" fn=this.viewStopActivity)
35
+ viewWaypointLabel=(hash label=(t "order.fields.get-label") fn=this.viewWaypointLabel)
36
+ }}
37
+ />
30
38
  </div>
31
39
  {{else}}
32
40
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-2">
33
41
  <div class="field-info-container">
34
42
  <label class="field-name">{{t "order.fields.pickup"}}</label>
35
- <DisplayPlace @place={{@resource.payload.pickup}} @eta={{get @resource.eta @resource.payload.pickup.id}} />
43
+ <DisplayPlace @place={{@resource.payload.pickup}} @eta={{if this.showRouteEtaData (get @resource.eta @resource.payload.pickup.id)}} />
36
44
  </div>
37
45
  <div class="field-info-container">
38
46
  <label class="field-name">{{t "order.fields.dropoff"}}</label>
39
- <DisplayPlace @place={{@resource.payload.dropoff}} @eta={{get @resource.eta @resource.payload.dropoff.id}} />
47
+ <DisplayPlace @place={{@resource.payload.dropoff}} @eta={{if this.showRouteEtaData (get @resource.eta @resource.payload.dropoff.id)}} />
40
48
  </div>
41
49
  <div class="field-info-container">
42
50
  <label class="field-name">{{t "order.fields.return"}}</label>
@@ -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();