@fleetbase/fleetops-engine 0.6.49 → 0.6.51

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 (271) hide show
  1. package/README.md +185 -67
  2. package/addon/components/contact/panel-header.hbs +11 -48
  3. package/addon/components/customer/create-order-form.hbs +2 -2
  4. package/addon/components/customer/details.hbs +1 -2
  5. package/addon/components/customer/form.hbs +62 -50
  6. package/addon/components/customer/form.js +27 -1
  7. package/addon/components/customer/order-form.hbs +2 -2
  8. package/addon/components/customer/orders.hbs +50 -2
  9. package/addon/components/customer/orders.js +67 -5
  10. package/addon/components/display-place.hbs +2 -2
  11. package/addon/components/driver/form.hbs +1 -1
  12. package/addon/components/home/getting-started-guidance.hbs +113 -0
  13. package/addon/components/home/getting-started-guidance.js +107 -0
  14. package/addon/components/issue/details/comments.hbs +3 -0
  15. package/addon/components/issue/details/comments.js +3 -0
  16. package/addon/components/issue/details/correspondence.hbs +9 -0
  17. package/addon/components/issue/details/correspondence.js +83 -0
  18. package/addon/components/issue/details/documents.hbs +69 -0
  19. package/addon/components/issue/details/documents.js +85 -0
  20. package/addon/components/issue/details.hbs +220 -59
  21. package/addon/components/issue/details.js +245 -0
  22. package/addon/components/issue/form.hbs +19 -1
  23. package/addon/components/issue/panel-header.hbs +43 -0
  24. package/addon/components/issue/panel-header.js +22 -0
  25. package/addon/components/issue/timeline.hbs +40 -0
  26. package/addon/components/issue/timeline.js +45 -0
  27. package/addon/components/leaflet-tracking-marker.js +3 -0
  28. package/addon/components/map/drawer/position-listing.js +3 -0
  29. package/addon/components/map/google-live-map.js +6 -2
  30. package/addon/components/map/leaflet-live-map.hbs +48 -14
  31. package/addon/components/map/leaflet-live-map.js +201 -20
  32. package/addon/components/map/order-list-overlay.hbs +4 -4
  33. package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
  34. package/addon/components/modals/convert-customer-to-vendor.js +55 -0
  35. package/addon/components/modals/issue-assign.hbs +20 -0
  36. package/addon/components/modals/issue-assign.js +49 -0
  37. package/addon/components/modals/issue-close.hbs +30 -0
  38. package/addon/components/modals/issue-close.js +73 -0
  39. package/addon/components/modals/issue-status.hbs +20 -0
  40. package/addon/components/modals/issue-status.js +43 -0
  41. package/addon/components/modals/order-change-destination.hbs +46 -0
  42. package/addon/components/modals/order-change-destination.js +106 -0
  43. package/addon/components/modals/reset-customer-credentials.js +12 -6
  44. package/addon/components/modals/stop-activity.hbs +28 -0
  45. package/addon/components/modals/stop-activity.js +52 -0
  46. package/addon/components/modals/update-order-activity.hbs +78 -2
  47. package/addon/components/modals/update-order-activity.js +197 -5
  48. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  49. package/addon/components/orchestrator/resource-panel.js +2 -4
  50. package/addon/components/order/activity-list.hbs +2 -2
  51. package/addon/components/order/activity-timeline.hbs +31 -29
  52. package/addon/components/order/details/activity.hbs +3 -3
  53. package/addon/components/order/details/activity.js +13 -2
  54. package/addon/components/order/details/documents.js +2 -0
  55. package/addon/components/order/details/proof.hbs +40 -38
  56. package/addon/components/order/details/proof.js +31 -4
  57. package/addon/components/order/details/purchase-rate.hbs +2 -2
  58. package/addon/components/order/details/route.hbs +11 -3
  59. package/addon/components/order/details/route.js +91 -14
  60. package/addon/components/order/details/tracking.hbs +101 -72
  61. package/addon/components/order/details/tracking.js +149 -2
  62. package/addon/components/order/details.hbs +5 -5
  63. package/addon/components/order/details.js +19 -1
  64. package/addon/components/order/form/details.hbs +3 -3
  65. package/addon/components/order/form/documents.js +11 -4
  66. package/addon/components/order/form/route.hbs +108 -104
  67. package/addon/components/order/form/route.js +37 -1
  68. package/addon/components/order/form/service-rate.hbs +2 -2
  69. package/addon/components/order/header.hbs +1 -1
  70. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  71. package/addon/components/order-config-manager/activity-flow.js +178 -12
  72. package/addon/components/place/address.js +1 -1
  73. package/addon/components/route-list.hbs +17 -29
  74. package/addon/components/route-list.js +42 -10
  75. package/addon/components/service-rate/details.hbs +42 -1
  76. package/addon/components/service-rate/form.hbs +119 -1
  77. package/addon/components/service-rate/form.js +59 -0
  78. package/addon/components/vehicle/form.js +1 -1
  79. package/addon/components/vehicle/pill.hbs +1 -1
  80. package/addon/components/vendor/personnel-manager.hbs +166 -0
  81. package/addon/components/vendor/personnel-manager.js +143 -0
  82. package/addon/components/widget/live-fleet.hbs +1 -0
  83. package/addon/components/widget/live-fleet.js +2 -7
  84. package/addon/components/widget/revenue-trend.hbs +1 -1
  85. package/addon/components/widget/revenue-trend.js +75 -2
  86. package/addon/controllers/management/contacts/customers/details.js +31 -13
  87. package/addon/controllers/management/contacts/customers.js +16 -5
  88. package/addon/controllers/management/contacts/index/details.js +31 -13
  89. package/addon/controllers/management/contacts/index.js +7 -0
  90. package/addon/controllers/management/issues/index/details.js +52 -6
  91. package/addon/controllers/management/vehicles/index/new.js +1 -1
  92. package/addon/controllers/management/vendors/index/details.js +4 -0
  93. package/addon/controllers/operations/orders/index/details.js +15 -7
  94. package/addon/controllers/operations/orders/index.js +15 -1
  95. package/addon/extension.js +19 -12
  96. package/addon/helpers/format-duration.js +1 -38
  97. package/addon/helpers/place-address-html.js +2 -2
  98. package/addon/initializers/load-leaflet-assets.js +5 -6
  99. package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
  100. package/addon/routes/management/issues/index/details.js +1 -1
  101. package/addon/routes/operations/service-rates/index.js +1 -1
  102. package/addon/routes.js +1 -0
  103. package/addon/services/contact-actions.js +231 -1
  104. package/addon/services/geofence.js +163 -19
  105. package/addon/services/getting-started.js +44 -0
  106. package/addon/services/issue-actions.js +71 -0
  107. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  108. package/addon/services/leaflet-map-manager.js +2 -0
  109. package/addon/services/map-adapter/leaflet.js +19 -0
  110. package/addon/services/service-areas.js +3 -0
  111. package/addon/services/vehicle-actions.js +1 -1
  112. package/addon/styles/fleetops-engine.css +1127 -1
  113. package/addon/templates/management/contacts/customers/details.hbs +1 -1
  114. package/addon/templates/management/contacts/index/details.hbs +1 -1
  115. package/addon/templates/management/contacts.hbs +2 -2
  116. package/addon/templates/management/issues/index/details/index.hbs +3 -1
  117. package/addon/templates/management/issues/index/details.hbs +3 -2
  118. package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
  119. package/addon/templates/operations/orders/index/details/index.hbs +2 -2
  120. package/addon/templates/operations/orders/index.hbs +1 -1
  121. package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
  122. package/addon/templates/operations/service-rates/index/new.hbs +1 -0
  123. package/addon/templates/settings/payments/onboard.hbs +8 -8
  124. package/addon/utils/fleet-ops-options.js +2 -1
  125. package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
  126. package/addon/utils/leaflet-plugin-loader.js +219 -0
  127. package/addon/utils/normalize-order-config-flow.js +41 -0
  128. package/addon/utils/place-address-html.js +3 -2
  129. package/addon/utils/route-visualization.js +54 -7
  130. package/addon/utils/setup-customer-portal.js +4 -64
  131. package/app/components/home/getting-started-guidance.js +1 -0
  132. package/app/components/issue/details/comments.js +1 -0
  133. package/app/components/issue/details/correspondence.js +1 -0
  134. package/app/components/issue/details/documents.js +1 -0
  135. package/app/components/issue/panel-header.js +1 -0
  136. package/app/components/modals/convert-customer-to-vendor.js +1 -0
  137. package/app/components/modals/issue-assign.js +1 -0
  138. package/app/components/modals/issue-close.js +1 -0
  139. package/app/components/modals/issue-status.js +1 -0
  140. package/app/components/modals/order-change-destination.js +1 -0
  141. package/app/components/modals/stop-activity.js +1 -0
  142. package/app/components/vendor/personnel-manager.js +1 -0
  143. package/app/helpers/format-duration.js +1 -1
  144. package/app/services/getting-started.js +1 -0
  145. package/app/utils/leaflet-draw-namespace-guard.js +1 -0
  146. package/app/utils/leaflet-plugin-loader.js +1 -0
  147. package/composer.json +1 -1
  148. package/extension.json +1 -1
  149. package/package.json +5 -5
  150. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  151. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  152. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  153. package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
  154. package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
  155. package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
  156. package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
  157. package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
  158. package/server/resources/views/labels/default.php +57 -49
  159. package/server/resources/views/labels/waypoint-label.php +27 -20
  160. package/server/resources/views/mail/customer-credentials.blade.php +16 -13
  161. package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
  162. package/server/seeders/Testing/FleetSeeder.php +22 -22
  163. package/server/seeders/Testing/OrdersSeeder.php +6 -22
  164. package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
  165. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  166. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  167. package/server/src/Expansions/UserFilterExpansion.php +28 -0
  168. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  169. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  170. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  171. package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
  172. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  173. package/server/src/Http/Controllers/Api/v1/OrderController.php +109 -80
  174. package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
  175. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  176. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  177. package/server/src/Http/Controllers/Internal/v1/ContactController.php +154 -0
  178. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
  179. package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
  180. package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
  181. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  182. package/server/src/Http/Controllers/Internal/v1/OrderController.php +530 -52
  183. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
  184. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  185. package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
  186. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  187. package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
  188. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  189. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  190. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  191. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  192. package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
  193. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  194. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  195. package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
  196. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  197. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  198. package/server/src/Http/Resources/v1/Customer.php +79 -0
  199. package/server/src/Http/Resources/v1/Index/Order.php +12 -9
  200. package/server/src/Http/Resources/v1/Issue.php +25 -0
  201. package/server/src/Http/Resources/v1/Order.php +20 -18
  202. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  203. package/server/src/Http/Resources/v1/Payload.php +23 -20
  204. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  205. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  206. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  207. package/server/src/Mail/CustomerCredentialsMail.php +5 -6
  208. package/server/src/Models/Contact.php +122 -66
  209. package/server/src/Models/Customer.php +64 -0
  210. package/server/src/Models/Driver.php +12 -3
  211. package/server/src/Models/Entity.php +2 -2
  212. package/server/src/Models/Issue.php +47 -6
  213. package/server/src/Models/Order.php +62 -10
  214. package/server/src/Models/Payload.php +17 -1
  215. package/server/src/Models/ServiceArea.php +1 -1
  216. package/server/src/Models/ServiceRate.php +273 -1
  217. package/server/src/Models/ServiceRateFee.php +39 -9
  218. package/server/src/Models/TrackingNumber.php +8 -2
  219. package/server/src/Models/Vehicle.php +18 -2
  220. package/server/src/Models/VendorPersonnel.php +10 -1
  221. package/server/src/Models/Waypoint.php +2 -2
  222. package/server/src/Models/Zone.php +1 -1
  223. package/server/src/Notifications/LateDeparture.php +2 -2
  224. package/server/src/Notifications/ProlongedStoppage.php +2 -2
  225. package/server/src/Notifications/RouteDeviation.php +2 -2
  226. package/server/src/Observers/ContactObserver.php +2 -0
  227. package/server/src/Observers/PurchaseRateObserver.php +20 -0
  228. package/server/src/Observers/ServiceRateObserver.php +6 -6
  229. package/server/src/Observers/ZoneObserver.php +56 -0
  230. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
  231. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  232. package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
  233. package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
  234. package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
  235. package/server/src/Support/Analytics/RevenueTrend.php +0 -1
  236. package/server/src/Support/Analytics/TopDrivers.php +10 -2
  237. package/server/src/Support/CustomerAuth.php +98 -0
  238. package/server/src/Support/GettingStarted.php +144 -0
  239. package/server/src/Support/LabelPdf.php +52 -0
  240. package/server/src/Support/Metrics/AbstractMetric.php +4 -4
  241. package/server/src/Support/PlaceSearch.php +282 -0
  242. package/server/src/Support/ResolvesOrderServiceStops.php +522 -0
  243. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  244. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  245. package/server/src/Tracking/TrackingStop.php +13 -11
  246. package/server/src/routes.php +76 -10
  247. package/server/tests/AnalyticsRoutesTest.php +8 -0
  248. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  249. package/server/tests/CustomerContactIdentitySafetyTest.php +95 -0
  250. package/server/tests/CustomerEndpointTest.php +199 -0
  251. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  252. package/server/tests/GettingStartedStatusTest.php +21 -0
  253. package/server/tests/LabelPdfRenderingTest.php +23 -0
  254. package/server/tests/LiveControllerViewportTest.php +60 -0
  255. package/server/tests/MetricsRegistryTest.php +4 -8
  256. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  257. package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
  258. package/server/tests/OrderConfigEndpointTest.php +86 -0
  259. package/server/tests/OrderFileAttachmentTest.php +20 -0
  260. package/server/tests/ServiceAreaZoneApiShapeTest.php +78 -0
  261. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  262. package/translations/ar-ae.yml +9 -0
  263. package/translations/bg-bg.yaml +9 -0
  264. package/translations/en-us.yaml +12 -0
  265. package/translations/es-pa.yaml +9 -0
  266. package/translations/fr-fr.yaml +9 -0
  267. package/translations/mn-mn.yaml +9 -0
  268. package/translations/pt-br.yaml +9 -0
  269. package/translations/ru-ru.yaml +9 -0
  270. package/translations/vi-vn.yaml +9 -0
  271. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -1,16 +1,23 @@
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 = [];
19
+ @tracked proof;
20
+ @tracked bypassProof = false;
14
21
 
15
22
  constructor(owner, { options = {} }) {
16
23
  super(...arguments);
@@ -18,9 +25,103 @@ export default class ModalsUpdateOrderActivityComponent extends Component {
18
25
  this.loadActivity.perform();
19
26
  }
20
27
 
28
+ get waypoints() {
29
+ const waypoints = this.order?.payload?.waypoints;
30
+ if (typeof waypoints?.toArray === 'function') {
31
+ return waypoints.toArray();
32
+ }
33
+
34
+ return Array.isArray(waypoints) ? waypoints : Array.from(waypoints ?? []);
35
+ }
36
+
37
+ get serviceStops() {
38
+ return buildServiceStopsFromPayload(this.order?.payload);
39
+ }
40
+
41
+ get activeStopId() {
42
+ const activeStop = this.order?.tracker_data?.active_stop;
43
+
44
+ return activeStop?.uuid ?? activeStop?.public_id ?? this.order?.payload?.current_waypoint_uuid ?? this.order?.payload?.current_waypoint;
45
+ }
46
+
47
+ get isMultipleWaypointOrder() {
48
+ return this.waypoints.length > 0;
49
+ }
50
+
51
+ get orderHasStarted() {
52
+ const status = this.order?.status;
53
+
54
+ return Boolean(this.order?.started || this.order?.started_at || (status && !['created', 'dispatched'].includes(status)));
55
+ }
56
+
57
+ get shouldUseWaypointActivity() {
58
+ return this.isMultipleWaypointOrder && this.orderHasStarted;
59
+ }
60
+
61
+ get currentWaypoint() {
62
+ if (!this.shouldUseWaypointActivity) {
63
+ return null;
64
+ }
65
+
66
+ const currentWaypointId = this.activeStopId;
67
+
68
+ if (currentWaypointId) {
69
+ const stop = this.serviceStops.find(({ place }) => {
70
+ return [place?.id, place?.uuid, place?.public_id, place?.waypoint_public_id].includes(currentWaypointId);
71
+ });
72
+
73
+ if (stop?.place) {
74
+ return stop.place;
75
+ }
76
+ }
77
+
78
+ return this.serviceStops.find(({ place }) => !place?.complete)?.place ?? this.serviceStops[0]?.place;
79
+ }
80
+
81
+ get currentWaypointId() {
82
+ const waypoint = this.currentWaypoint;
83
+
84
+ return waypoint?.id ?? waypoint?.public_id ?? waypoint?.uuid;
85
+ }
86
+
87
+ get currentWaypointProofSubjectId() {
88
+ const waypoint = this.currentWaypoint;
89
+
90
+ return waypoint?.waypoint_public_id ?? waypoint?.id ?? waypoint?.public_id ?? waypoint?.uuid;
91
+ }
92
+
93
+ get selectedActivityRecord() {
94
+ if (this.selectedActivity === 'custom') {
95
+ return this.customActivity;
96
+ }
97
+
98
+ return this.activityOptions[this.selectedActivity];
99
+ }
100
+
101
+ get selectedActivityRequiresPod() {
102
+ return Boolean(this.selectedActivityRecord?.require_pod);
103
+ }
104
+
105
+ get acceptedProofFileTypes() {
106
+ return ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
107
+ }
108
+
109
+ get hasUploadedProof() {
110
+ return Boolean(this.proof?.id || this.proof?.public_id);
111
+ }
112
+
113
+ get uploadedProofId() {
114
+ return this.proof?.id ?? this.proof?.public_id;
115
+ }
116
+
21
117
  @task *loadActivity() {
118
+ if (!this.order?.id) {
119
+ return [];
120
+ }
121
+
22
122
  try {
23
- const activityOptions = yield this.fetch.get(`orders/next-activity/${this.order.id}`);
123
+ const params = this.shouldUseWaypointActivity && this.currentWaypointId ? { waypoint: this.currentWaypointId } : {};
124
+ const activityOptions = yield this.fetch.get(`orders/next-activity/${this.order.id}`, params);
24
125
  this.activityOptions = activityOptions;
25
126
  return activityOptions;
26
127
  } catch (err) {
@@ -31,21 +132,112 @@ export default class ModalsUpdateOrderActivityComponent extends Component {
31
132
  @task *submitActivity(modal) {
32
133
  modal.startLoading();
33
134
 
34
- const activity = this.selectedActivity === 'custom' ? this.customActivity : this.activityOptions[this.selectedActivity];
135
+ const activity = this.selectedActivityRecord;
35
136
  if (this.selectedActivity === 'custom' && !activity.status && !activity.details && !activity.code) {
36
137
  modal.stopLoading();
37
138
  return this.notifications.warning(this.intl.t('fleet-ops.operations.orders.index.view.invalid-warning'));
38
139
  }
39
140
 
141
+ if (this.selectedActivityRequiresPod && this.uploadProofFile.isRunning) {
142
+ modal.stopLoading();
143
+ return this.notifications.warning('Photo proof of delivery is still uploading.');
144
+ }
145
+
146
+ if (this.selectedActivityRequiresPod && !this.hasUploadedProof && !this.bypassProof) {
147
+ modal.stopLoading();
148
+ return this.notifications.warning('Upload photo proof of delivery or choose to bypass proof of delivery.');
149
+ }
150
+
40
151
  try {
41
- yield this.fetch.patch(`orders/update-activity/${this.order.id}`, { activity });
152
+ yield this.fetch.patch(`orders/update-activity/${this.order.id}`, {
153
+ activity,
154
+ proof: this.hasUploadedProof ? this.uploadedProofId : null,
155
+ bypass_proof: this.selectedActivityRequiresPod && this.bypassProof,
156
+ });
157
+
158
+ if (typeof this.order?.reload === 'function') {
159
+ yield this.order.reload();
160
+ }
161
+
162
+ if (typeof this.order?.loadTrackerData === 'function') {
163
+ if (typeof this.order.set === 'function') {
164
+ this.order.set('tracker_data', null);
165
+ }
166
+
167
+ yield this.order.loadTrackerData();
168
+ }
169
+
42
170
  this.modalsManager.setOption('activityCreated', activity);
171
+ this.modalsManager.setOption('proofCreated', this.hasUploadedProof ? this.proof : null);
43
172
  this.notifications.success(`Order activity has been updated to ${activity.status}`);
173
+ modal.done();
44
174
  } catch (error) {
45
175
  this.notifications.serverError(error);
46
176
  } finally {
47
177
  modal.stopLoading();
48
- modal.done();
49
178
  }
50
179
  }
180
+
181
+ @task *capturePhotoProof() {
182
+ const [proofFile] = this.proofFiles;
183
+ if (!proofFile) {
184
+ return null;
185
+ }
186
+
187
+ const headers = this.fetch.getHeaders();
188
+ delete headers['Content-Type'];
189
+
190
+ const subjectId = this.currentWaypointProofSubjectId;
191
+ const subjectPath = subjectId ? `/${subjectId}` : '';
192
+ const url = [get(config, 'API.host'), get(config, 'API.namespace'), `orders/${this.order.id}/capture-photo${subjectPath}`].filter(Boolean).join('/');
193
+
194
+ const response = yield proofFile.upload(url, {
195
+ data: {
196
+ remarks: 'Verified by Photo',
197
+ },
198
+ headers,
199
+ withCredentials: true,
200
+ });
201
+
202
+ return response.json();
203
+ }
204
+
205
+ @task *uploadProofFile(file) {
206
+ if (!file) {
207
+ return;
208
+ }
209
+
210
+ this.bypassProof = false;
211
+ this.proof = null;
212
+ this.proofFiles = [file];
213
+
214
+ try {
215
+ this.proof = this.normalizeProofResponse(yield this.capturePhotoProof.perform());
216
+ this.notifications.success('Photo proof of delivery uploaded.');
217
+ } catch (error) {
218
+ this.proof = null;
219
+ this.proofFiles = [];
220
+ this.notifications.serverError(error);
221
+ }
222
+ }
223
+
224
+ @action toggleBypassProof(value) {
225
+ this.bypassProof = value;
226
+ if (value) {
227
+ this.proofFiles = [];
228
+ this.proof = null;
229
+ }
230
+ }
231
+
232
+ normalizeProofResponse(response) {
233
+ if (response?.proof) {
234
+ return response.proof;
235
+ }
236
+
237
+ if (response?.data?.id || response?.data?.public_id) {
238
+ return response.data;
239
+ }
240
+
241
+ return response;
242
+ }
51
243
  }
@@ -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;
@@ -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 [
@@ -20,9 +31,9 @@ export default class OrderDetailsActivityComponent extends Component {
20
31
  disabled: this.args.resource.status === 'canceled',
21
32
  onClick: () => {
22
33
  this.orderActions.updateActivity(this.args.resource, {
23
- onFinish: ({ activityCreated }) => {
34
+ onFinish: (options) => {
24
35
  if (typeof this.args.onChange === 'function') {
25
- this.args.onChange(activityCreated);
36
+ this.args.onChange(options);
26
37
  }
27
38
  },
28
39
  });
@@ -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) => {
@@ -1,42 +1,44 @@
1
- <ContentPanel @title={{t "order.fields.proof-of-delivery"}} @isLoading={{@isLoading}} @open={{true}} @wrapperClass="bordered-top">
2
- {{#if this.proofs}}
3
- <div class="grid grid-cols-1 lg:grid-cols-3 gap-3">
4
- {{#each this.proofs as |proof|}}
5
- <div>
6
- {{#if (eq proof.type "photo")}}
7
- <a href={{proof.url}} target={{proof.id}}>
8
- <Image src={{proof.url}} class="w-48 h-48 rounded-md shadow-sm mb-2" />
9
- </a>
10
- {{/if}}
11
- {{#if (eq proof.type "signature")}}
12
- <a href={{proof.raw}} target={{proof.id}} download>
13
- <Image src={{proof.raw}} class="w-48 h-48 rounded-md shadow-sm mb-2" />
14
- </a>
15
- {{/if}}
16
- {{#if (eq proof.type "scan")}}
17
- <div class="flex items-center mb-2">
18
- <FaIcon @icon="qrcode" @size="2xl" class="text-blue-400" />
19
- </div>
20
- {{/if}}
21
- <div class="font-semibold text-sm mb-0.5">{{proof.remarks}}</div>
22
- <div class="text-xs">{{proof.public_id}}</div>
23
- <div class="text-xs">{{format-date-fns proof.created_at}}</div>
24
- </div>
25
- {{/each}}
26
- </div>
27
- {{else}}
28
- <div class="min-h-24 p-4">
29
- <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">
30
- <div class="text-center">
31
- <FaIcon @icon="camera-retro" @size="2x" class="text-gray-400 mb-2" />
32
- <div class="text-sm text-gray-600 dark:text-gray-400 font-medium">
33
- No proof of delivery
1
+ <div {{did-update this.reloadProofs @reloadToken}}>
2
+ <ContentPanel @title={{t "order.fields.proof-of-delivery"}} @isLoading={{@isLoading}} @open={{true}} @wrapperClass="bordered-top">
3
+ {{#if this.proofs}}
4
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-3">
5
+ {{#each this.proofs as |proof|}}
6
+ <div>
7
+ {{#if (eq proof.type "photo")}}
8
+ <a href={{proof.url}} target={{proof.id}}>
9
+ <Image src={{proof.url}} class="w-48 h-48 rounded-md shadow-sm mb-2" />
10
+ </a>
11
+ {{/if}}
12
+ {{#if (eq proof.type "signature")}}
13
+ <a href={{proof.raw}} target={{proof.id}} download>
14
+ <Image src={{proof.raw}} class="w-48 h-48 rounded-md shadow-sm mb-2" />
15
+ </a>
16
+ {{/if}}
17
+ {{#if (eq proof.type "scan")}}
18
+ <div class="flex items-center mb-2">
19
+ <FaIcon @icon="qrcode" @size="2xl" class="text-blue-400" />
20
+ </div>
21
+ {{/if}}
22
+ <div class="font-semibold text-sm mb-0.5">{{proof.remarks}}</div>
23
+ <div class="text-xs">{{proof.public_id}}</div>
24
+ <div class="text-xs">{{format-date-fns proof.created_at}}</div>
34
25
  </div>
35
- <div class="text-xs text-gray-500 dark:text-gray-500 mt-1">
36
- Captured proof of delivery will show up here
26
+ {{/each}}
27
+ </div>
28
+ {{else}}
29
+ <div class="min-h-24 p-4">
30
+ <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">
31
+ <div class="text-center">
32
+ <FaIcon @icon="camera-retro" @size="2x" class="text-gray-400 mb-2" />
33
+ <div class="text-sm text-gray-600 dark:text-gray-400 font-medium">
34
+ No proof of delivery
35
+ </div>
36
+ <div class="text-xs text-gray-500 dark:text-gray-500 mt-1">
37
+ Captured proof of delivery will show up here
38
+ </div>
37
39
  </div>
38
40
  </div>
39
41
  </div>
40
- </div>
41
- {{/if}}
42
- </ContentPanel>
42
+ {{/if}}
43
+ </ContentPanel>
44
+ </div>
@@ -1,5 +1,6 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
3
4
  import { inject as service } from '@ember/service';
4
5
  import { task } from 'ember-concurrency-decorators';
5
6
 
@@ -13,12 +14,18 @@ export default class OrderDetailsProofComponent extends Component {
13
14
  }
14
15
 
15
16
  @task *loadOrderProofs(order) {
17
+ if (!order?.id) {
18
+ this.proofs = [];
19
+ return;
20
+ }
21
+
16
22
  const proofs = yield this.fetch.get(`orders/${order.id}/proofs`);
17
23
 
18
- this.proofs = proofs.map((proof) => ({
19
- ...proof,
20
- type: this.#getTypeFromRemarks(proof.remarks),
21
- }));
24
+ this.proofs = this.#normalizeProofs(proofs);
25
+ }
26
+
27
+ @action reloadProofs() {
28
+ this.loadOrderProofs.perform(this.args.resource);
22
29
  }
23
30
 
24
31
  #getTypeFromRemarks(remarks = '') {
@@ -27,4 +34,24 @@ export default class OrderDetailsProofComponent extends Component {
27
34
  if (remarks.endsWith('Signature')) return 'signature';
28
35
  return undefined;
29
36
  }
37
+
38
+ #normalizeProofs(proofs = []) {
39
+ const seen = new Set();
40
+
41
+ return proofs
42
+ .filter((proof) => {
43
+ const key = proof.uuid ?? proof.public_id ?? proof.id;
44
+ if (!key || seen.has(key)) {
45
+ return false;
46
+ }
47
+
48
+ seen.add(key);
49
+ return true;
50
+ })
51
+ .map((proof) => ({
52
+ ...proof,
53
+ type: this.#getTypeFromRemarks(proof.remarks),
54
+ }))
55
+ .sort((a, b) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
56
+ }
30
57
  }
@@ -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>