@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
@@ -0,0 +1,106 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { action } from '@ember/object';
5
+ import { task } from 'ember-concurrency';
6
+ import { buildServiceStopsFromPayload, describeRoutePoint } from '../../utils/route-visualization';
7
+ import { colorForId } from '../../utils/route-colors';
8
+
9
+ export default class ModalsOrderChangeDestinationComponent extends Component {
10
+ @service fetch;
11
+ @service notifications;
12
+ @tracked selectedOption;
13
+
14
+ get order() {
15
+ return this.args.options?.order;
16
+ }
17
+
18
+ get waypoints() {
19
+ const waypoints = this.order?.payload?.waypoints;
20
+ if (typeof waypoints?.toArray === 'function') {
21
+ return waypoints.toArray();
22
+ }
23
+
24
+ return Array.isArray(waypoints) ? waypoints : Array.from(waypoints ?? []);
25
+ }
26
+
27
+ get routeColor() {
28
+ return colorForId(this.order?.public_id ?? this.order?.id ?? 'order-route');
29
+ }
30
+
31
+ get serviceStops() {
32
+ return buildServiceStopsFromPayload(this.order?.payload);
33
+ }
34
+
35
+ get currentWaypointId() {
36
+ const activeStop = this.order?.tracker_data?.active_stop;
37
+ const payload = this.order?.payload;
38
+
39
+ return activeStop?.uuid ?? activeStop?.public_id ?? payload?.current_waypoint_uuid ?? payload?.current_waypoint;
40
+ }
41
+
42
+ get waypointOptions() {
43
+ return this.serviceStops.map((stop, index) => ({
44
+ waypoint: stop.place,
45
+ stop,
46
+ index,
47
+ id: this.waypointId(stop.place),
48
+ isCurrent: this.isCurrentWaypoint(stop.place),
49
+ isSelected: this.isSelectedStop(stop),
50
+ label: describeRoutePoint(stop, this.routeColor).label,
51
+ title: describeRoutePoint(stop, this.routeColor).title,
52
+ tracking: this.trackingForStop(stop),
53
+ }));
54
+ }
55
+
56
+ waypointId(waypoint) {
57
+ return waypoint?.id ?? waypoint?.public_id ?? waypoint?.uuid;
58
+ }
59
+
60
+ isCurrentWaypoint(waypoint) {
61
+ return [waypoint?.id, waypoint?.uuid, waypoint?.public_id, waypoint?.waypoint_public_id].includes(this.currentWaypointId);
62
+ }
63
+
64
+ isSelectedStop(stop) {
65
+ const selectedPlace = this.selectedOption?.waypoint;
66
+
67
+ return [selectedPlace?.id, selectedPlace?.uuid, selectedPlace?.public_id, selectedPlace?.waypoint_public_id].includes(this.waypointId(stop?.place));
68
+ }
69
+
70
+ trackingForStop(stop) {
71
+ return stop?.place?.tracking ?? stop?.place?.tracking_number ?? stop?.trackingNumberUuid ?? this.order?.tracking ?? this.order?.tracking_number;
72
+ }
73
+
74
+ @action selectOption(option) {
75
+ if (!option?.isCurrent) {
76
+ this.selectedOption = option;
77
+ }
78
+ }
79
+
80
+ @task *confirmSelection(modal) {
81
+ const option = this.selectedOption;
82
+ const waypoint = option?.waypoint;
83
+ const waypointId = this.waypointId(waypoint);
84
+ if (!waypointId) {
85
+ modal.stopLoading();
86
+ return this.notifications.warning('Select a destination first.');
87
+ }
88
+
89
+ modal.startLoading();
90
+
91
+ try {
92
+ const order = yield this.fetch.patch(`orders/set-destination/${this.order.id}/${waypointId}`);
93
+ this.notifications.success(`Current destination changed to ${waypoint.name ?? waypoint.address ?? waypointId}.`);
94
+
95
+ if (typeof this.args.options?.onChange === 'function') {
96
+ yield this.args.options.onChange(order, waypoint);
97
+ }
98
+
99
+ modal.done();
100
+ } catch (error) {
101
+ this.notifications.serverError(error);
102
+ } finally {
103
+ modal.stopLoading();
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,28 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
+ <div class="modal-body-container">
3
+ <div class="mb-4 rounded-md border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-800">
4
+ <div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Stop</div>
5
+ <div class="mt-1 text-sm font-semibold text-gray-900 dark:text-gray-100">{{this.stopName}}</div>
6
+ <div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{or this.place.address this.place.street1}}</div>
7
+ </div>
8
+
9
+ {{#if this.loadActivity.isRunning}}
10
+ <div class="flex items-center justify-center p-8 text-sm text-gray-500 dark:text-gray-400">
11
+ <FaIcon @icon="spinner" @spin={{true}} class="mr-2" />
12
+ Loading activity...
13
+ </div>
14
+ {{else if this.trackingNumberUuid}}
15
+ <Order::ActivityList @activity={{this.activity}} @resource={{hash status=this.stop.status}} />
16
+ {{else}}
17
+ <div class="min-h-24 pb-4">
18
+ <div class="flex items-center justify-center rounded-xl border border-dashed border-gray-300 bg-gray-50 p-6 dark:border-gray-600 dark:bg-gray-800">
19
+ <div class="text-center">
20
+ <FaIcon @icon="layer-group" @size="2x" class="mb-2 text-gray-400" />
21
+ <div class="text-sm font-medium text-gray-600 dark:text-gray-400">No stop activity</div>
22
+ <div class="mt-1 text-xs text-gray-500 dark:text-gray-500">This stop does not have a separate activity stream.</div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ {{/if}}
27
+ </div>
28
+ </Modal::Default>
@@ -0,0 +1,52 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { task } from 'ember-concurrency';
5
+
6
+ export default class ModalsStopActivityComponent extends Component {
7
+ @service store;
8
+ @service notifications;
9
+ @tracked activity = [];
10
+
11
+ constructor() {
12
+ super(...arguments);
13
+ this.loadActivity.perform();
14
+ }
15
+
16
+ get stop() {
17
+ return this.args.options?.stop;
18
+ }
19
+
20
+ get place() {
21
+ return this.stop?.place ?? this.stop;
22
+ }
23
+
24
+ get order() {
25
+ return this.args.options?.order;
26
+ }
27
+
28
+ get stopName() {
29
+ return this.place?.name ?? this.place?.address ?? this.place?.id;
30
+ }
31
+
32
+ get trackingNumberUuid() {
33
+ return this.stop?.trackingNumberUuid ?? this.stop?.tracking_number_uuid ?? this.stop?.tracking_number?.uuid ?? this.place?.tracking_number_uuid ?? this.place?.tracking_number?.uuid;
34
+ }
35
+
36
+ @task *loadActivity() {
37
+ if (!this.trackingNumberUuid) {
38
+ this.activity = [];
39
+ return [];
40
+ }
41
+
42
+ try {
43
+ const activity = yield this.store.query('tracking-status', {
44
+ tracking_number_uuid: this.trackingNumberUuid,
45
+ });
46
+ this.activity = activity.toArray();
47
+ return activity;
48
+ } catch (error) {
49
+ this.notifications.serverError(error);
50
+ }
51
+ }
52
+ }
@@ -16,6 +16,23 @@
16
16
  </InfoBlock>
17
17
  {{/if}}
18
18
 
19
+ {{#if this.currentWaypoint}}
20
+ <div class="my-4 rounded-md border border-blue-200 bg-blue-50 px-4 py-3 text-blue-900 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-50">
21
+ <div class="text-xs font-semibold uppercase">Updating activity for</div>
22
+ <div class="mt-1 flex items-start justify-between gap-3">
23
+ <div class="min-w-0">
24
+ <div class="truncate text-sm font-semibold">{{or this.currentWaypoint.name this.currentWaypoint.id}}</div>
25
+ <div class="truncate text-xs">{{or (place-address-html this.currentWaypoint showTitle=false) this.currentWaypoint.address this.currentWaypoint.street1}}</div>
26
+ </div>
27
+ {{#if this.currentWaypoint.status_code}}
28
+ <Badge @status={{dasherize this.currentWaypoint.status_code}} class="uppercase">{{this.currentWaypoint.status_code}}</Badge>
29
+ {{else if this.currentWaypoint.status}}
30
+ <Badge @status={{dasherize this.currentWaypoint.status}} class="uppercase">{{this.currentWaypoint.status}}</Badge>
31
+ {{/if}}
32
+ </div>
33
+ </div>
34
+ {{/if}}
35
+
19
36
  <fieldset class="my-5">
20
37
  <legend class="sr-only">
21
38
  {{t "modals.order-new-activity.activity-options"}}
@@ -36,14 +53,18 @@
36
53
  </div>
37
54
  <label for={{concat "activity_" index}} class="ml-3 flex flex-col cursor-pointer w-full">
38
55
  <span class="block text-sm font-semibold">
39
- {{activity.status}}
56
+ {{or activity._resolved_status activity.status}}
40
57
  </span>
41
58
  <span class="block text-sm">
42
- {{activity.details}}
59
+ {{or activity._resolved_details activity.details}}
43
60
 
44
61
  {{#if (eq activity.code "dispatched")}}
45
62
  <InfoBlock @type="warning" @size="xs" @icon="exclamation-triangle" @text={{t "modals.order-new-activity.dispatch"}} class="mt-1" />
46
63
  {{/if}}
64
+
65
+ {{#if activity.require_pod}}
66
+ <InfoBlock @type="warning" @size="xs" @icon="camera" @text="Requires photo proof of delivery" class="mt-1" />
67
+ {{/if}}
47
68
  </span>
48
69
  </label>
49
70
  <div>
@@ -88,5 +109,26 @@
88
109
  </div>
89
110
  </div>
90
111
  </fieldset>
112
+
113
+ {{#if this.selectedActivityRequiresPod}}
114
+ <div class="mb-5 rounded-md border border-yellow-300 bg-yellow-50 p-4 dark:border-yellow-700 dark:bg-yellow-900">
115
+ <div class="mb-2 text-sm font-semibold text-yellow-900 dark:text-yellow-50">Photo proof of delivery required</div>
116
+ <p class="mb-3 text-xs text-yellow-800 dark:text-yellow-100">Upload a delivery photo before submitting this activity update.</p>
117
+ <FileUpload @name="photos[]" @accept={{join "," this.acceptedProofFileTypes}} @multiple={{false}} @onFileAdded={{this.setProofFile}}>
118
+ <a tabindex={{0}} class="btn btn-magic cursor-pointer">
119
+ <FaIcon @icon="camera" class="mr-1" />
120
+ Select photo
121
+ </a>
122
+ </FileUpload>
123
+ {{#if this.proofFiles}}
124
+ {{#each this.proofFiles as |file|}}
125
+ <div class="mt-3 flex items-center justify-between rounded-md border border-yellow-400 bg-white px-3 py-2 text-xs text-yellow-900 dark:bg-yellow-950 dark:text-yellow-50">
126
+ <span class="truncate">{{file.name}}</span>
127
+ <span class="font-semibold">{{round file.progress}}%</span>
128
+ </div>
129
+ {{/each}}
130
+ {{/if}}
131
+ </div>
132
+ {{/if}}
91
133
  </div>
92
- </Modal::Default>
134
+ </Modal::Default>
@@ -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
  }
@@ -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 [
@@ -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>