@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
@@ -27,6 +27,7 @@
27
27
  @onClick={{fn this.addWaypoint (hash)}}
28
28
  @helpText={{t "order.fields.add-waypoint-help-text"}}
29
29
  @permission={{cannot-write @resource}}
30
+ data-test-waypoint-add
30
31
  />
31
32
  {{/if}}
32
33
  </div>
@@ -34,118 +35,123 @@
34
35
 
35
36
  {{#if this.multipleWaypoints}}
36
37
  <DragSortList class="multi-drop-select-container overflow-visible" @items={{@resource.payload.waypoints}} @dragEndAction={{this.sortWaypoints}} as |waypoint index|>
37
- <div id={{concat "waypoint_" (add index 1)}} class="border rounded-md px-3 py-2 shadow-sm border-gray-300 dark:border-gray-900 mb-3">
38
- <div class="flex items-start justify-between">
39
- <div class="flex items-center justify-between grip-cursor">
40
- <FaIcon @icon="grip-lines" @prefix="fas" class="mr-3 text-gray-100" />
41
- <label class="waypoint-label dark:text-gray-100 text-sm">
42
- {{waypoint-label (add index 1)}}
43
- </label>
44
- </div>
45
-
46
- <div class="flex-1 flex-col px-3">
47
- <div class="mb-2">
48
- <ModelSelect
49
- @modelName="place"
50
- @selectedModel={{waypoint.place}}
51
- @placeholder={{concat (t "order.fields.select-waypoint") " " (add index 1)}}
52
- @triggerClass="form-select form-input truncate max-w-300px"
53
- @infiniteScroll={{false}}
54
- @customSearchEndpoint="places/search"
55
- @query={{hash geo=true latitude=this.location.latitude longitude=this.location.longitude}}
56
- @renderInPlace={{true}}
57
- @onChange={{fn this.setWaypointPlace index}}
58
- @permission={{cannot-write @resource}}
59
- as |place|
60
- >
61
- <Place::Address @place={{place}} />
62
- </ModelSelect>
63
- {{#if (and @resource.facilitator.isIntegratedVendor (is-not-facilitator-supported-place @resource.facilitator waypoint.place))}}
64
- <InfoBlock
65
- class="my-2 px-4 py-1.5 danger"
66
- @blockClass="flex flex-row items-center"
67
- @text={{concat @resource.facilitator.name (t "order.fields.infoblock-text")}}
68
- />
69
- {{/if}}
70
- {{#if waypoint.place.hasInvalidCoordinates}}
71
- <div class="leading-5 text-sm text-red-400 mt-2">
72
- <FaIcon @icon="exclamation-triangle" class="mr-1" />
73
- {{t "order.fields.invalid-coordinates"}}
74
- </div>
75
- {{/if}}
38
+ {{#let (get this.waypointRouteStops index) as |stop|}}
39
+ <div
40
+ id={{concat "waypoint_" (add index 1)}}
41
+ class="fleetops-order-form-waypoint {{if stop.required 'fleetops-order-form-waypoint--required'}} border rounded-md px-3 py-2 shadow-sm border-gray-300 dark:border-gray-900 mb-3"
42
+ data-test-waypoint-row={{add index 1}}
43
+ >
44
+ {{#if stop.required}}
45
+ <div class="fleetops-order-form-waypoint__required-tab" data-test-required-waypoint-tab>Required</div>
46
+ {{/if}}
47
+ <div class="flex items-start justify-between">
48
+ <div class="fleetops-order-form-waypoint__marker grip-cursor">
49
+ <FaIcon @icon="grip-lines" @prefix="fas" class="text-gray-400" />
50
+ <span class="index-count fleetops-route-stop-badge fleetops-order-form-waypoint__badge" style={{stop.badgeStyle}}>{{stop.label}}</span>
76
51
  </div>
77
- <div>
78
- <ModelSelect
79
- @modelName="customer"
80
- @selectedModel={{waypoint.customer}}
81
- @placeholder={{t "order.fields.customer-placeholder"}}
82
- @triggerClass="form-select form-input truncate max-w-300px"
83
- @infiniteScroll={{false}}
84
- @renderInPlace={{true}}
85
- @allowClear={{true}}
86
- @onChange={{fn this.setWaypointCustomer waypoint}}
87
- @permission={{cannot-write @resource}}
88
- as |model|
89
- >
90
- <div class="flex items-center">
91
- <div class="w-7">
92
- <FaIcon @icon={{if model.isVendor "warehouse" "user"}} />
52
+
53
+ <div class="flex-1 flex-col px-3">
54
+ <div class="mb-2">
55
+ <ModelSelect
56
+ @modelName="place"
57
+ @selectedModel={{waypoint.place}}
58
+ @placeholder={{concat (t "order.fields.select-waypoint") " " (add index 1)}}
59
+ @triggerClass="form-select form-input truncate max-w-300px"
60
+ @infiniteScroll={{false}}
61
+ @customSearchEndpoint="places/search"
62
+ @query={{hash geo=true latitude=this.location.latitude longitude=this.location.longitude}}
63
+ @renderInPlace={{true}}
64
+ @onChange={{fn this.setWaypointPlace index}}
65
+ @permission={{cannot-write @resource}}
66
+ as |place|
67
+ >
68
+ <Place::Address @place={{place}} />
69
+ </ModelSelect>
70
+ {{#if (and @resource.facilitator.isIntegratedVendor (is-not-facilitator-supported-place @resource.facilitator waypoint.place))}}
71
+ <InfoBlock
72
+ class="my-2 px-4 py-1.5 danger"
73
+ @blockClass="flex flex-row items-center"
74
+ @text={{concat @resource.facilitator.name (t "order.fields.infoblock-text")}}
75
+ />
76
+ {{/if}}
77
+ {{#if waypoint.place.hasInvalidCoordinates}}
78
+ <div class="leading-5 text-sm text-red-400 mt-2">
79
+ <FaIcon @icon="exclamation-triangle" class="mr-1" />
80
+ {{t "order.fields.invalid-coordinates"}}
93
81
  </div>
94
- <span class="uppercase">
95
- {{model.name}}
96
- </span>
97
- </div>
98
- </ModelSelect>
99
- </div>
100
- <div>
101
- <div class="flex flex-row items-center space-x-4 text-sm mt-2">
102
- <div class={{if (eq waypoint.type "dropoff") "is-checked"}}>
103
- <div class="flex flex-row items-center">
104
- <RadioButton
105
- @radioClass="focus:ring-blue-500 h-4 w-4 text-blue-500"
106
- @radioId={{concat "waypoint_" index "_dropoff"}}
107
- @value="dropoff"
108
- @groupValue={{waypoint.type}}
109
- @name={{concat "waypoint_" index "_dropoff"}}
110
- @changed={{fn (mut waypoint.type)}}
111
- />
112
- <label for={{concat "waypoint_" index "_dropoff"}} class="ml-2">Dropoff</label>
82
+ {{/if}}
83
+ </div>
84
+ <div>
85
+ <ModelSelect
86
+ @modelName="customer"
87
+ @selectedModel={{waypoint.customer}}
88
+ @placeholder={{t "order.fields.customer-placeholder"}}
89
+ @triggerClass="form-select form-input truncate max-w-300px"
90
+ @infiniteScroll={{false}}
91
+ @renderInPlace={{true}}
92
+ @allowClear={{true}}
93
+ @onChange={{fn this.setWaypointCustomer waypoint}}
94
+ @permission={{cannot-write @resource}}
95
+ as |model|
96
+ >
97
+ <div class="flex items-center">
98
+ <div class="w-7">
99
+ <FaIcon @icon={{if model.isVendor "warehouse" "user"}} />
100
+ </div>
101
+ <span class="uppercase">
102
+ {{model.name}}
103
+ </span>
113
104
  </div>
114
- </div>
115
- <div class={{if (eq waypoint.type "pickup") "is-checked"}}>
116
- <div class="flex flex-row items-center">
117
- <RadioButton
118
- @radioClass="focus:ring-blue-500 h-4 w-4 text-blue-500"
119
- @radioId={{concat "waypoint_" index "_pickup"}}
120
- @value="pickup"
121
- @groupValue={{waypoint.type}}
122
- @name={{concat "waypoint_" index "_pickup"}}
123
- @changed={{fn (mut waypoint.type)}}
124
- />
125
- <label for={{concat "waypoint_" index "_pickup"}} class="ml-2">Pickup</label>
105
+ </ModelSelect>
106
+ </div>
107
+ <div>
108
+ <div class="flex flex-row items-center space-x-4 text-sm mt-2">
109
+ <div class={{if (eq waypoint.type "dropoff") "is-checked"}}>
110
+ <div class="flex flex-row items-center">
111
+ <RadioButton
112
+ @radioClass="focus:ring-blue-500 h-4 w-4 text-blue-500"
113
+ @radioId={{concat "waypoint_" index "_dropoff"}}
114
+ @value="dropoff"
115
+ @groupValue={{waypoint.type}}
116
+ @name={{concat "waypoint_" index "_dropoff"}}
117
+ @changed={{fn (mut waypoint.type)}}
118
+ />
119
+ <label for={{concat "waypoint_" index "_dropoff"}} class="ml-2">Dropoff</label>
120
+ </div>
121
+ </div>
122
+ <div class={{if (eq waypoint.type "pickup") "is-checked"}}>
123
+ <div class="flex flex-row items-center">
124
+ <RadioButton
125
+ @radioClass="focus:ring-blue-500 h-4 w-4 text-blue-500"
126
+ @radioId={{concat "waypoint_" index "_pickup"}}
127
+ @value="pickup"
128
+ @groupValue={{waypoint.type}}
129
+ @name={{concat "waypoint_" index "_pickup"}}
130
+ @changed={{fn (mut waypoint.type)}}
131
+ />
132
+ <label for={{concat "waypoint_" index "_pickup"}} class="ml-2">Pickup</label>
133
+ </div>
126
134
  </div>
127
135
  </div>
128
136
  </div>
129
137
  </div>
130
- </div>
131
138
 
132
- <div class="flex items-center space-x-2">
133
- {{#if waypoint.place}}
134
- <Button @icon="edit" @size="sm" @onClick={{fn this.editPlace waypoint.place}} @disabled={{cannot-write waypoint.place}} />
135
- {{/if}}
136
- {{#unless (eq index 0)}}
137
- <Button @type="danger" @icon="trash" @size="sm" @onClick={{fn this.removeWaypoint waypoint}} />
138
- {{/unless}}
139
+ <div class="flex items-center space-x-2">
140
+ {{#if waypoint.place}}
141
+ <Button @icon="edit" @size="sm" @onClick={{fn this.editPlace waypoint.place}} @disabled={{cannot-write waypoint.place}} />
142
+ {{/if}}
143
+ {{#unless (eq index 0)}}
144
+ <Button @type="danger" @icon="trash" @size="sm" @onClick={{fn this.removeWaypoint waypoint}} />
145
+ {{/unless}}
146
+ </div>
139
147
  </div>
140
148
  </div>
141
- </div>
149
+ {{/let}}
142
150
  </DragSortList>
143
151
  {{else}}
144
152
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-2 text-xs dark:text-gray-100">
145
- <InputGroup>
146
- <div class="flex items-center justify-between">
147
- <label>{{t "order.fields.pickup"}}</label>
148
-
153
+ <InputGroup @name={{t "order.fields.pickup"}} @required={{true}}>
154
+ <div class="flex items-center justify-end">
149
155
  <div class="flex-row space-x-1 pr-0.5">
150
156
  {{#if @resource.payload.pickup}}
151
157
  <a href="javascript:;" {{on "click" (fn this.editPlace @resource.payload.pickup)}}>
@@ -186,10 +192,8 @@
186
192
  {{/if}}
187
193
  </InputGroup>
188
194
 
189
- <InputGroup>
190
- <div class="flex items-center justify-between">
191
- <label>{{t "order.fields.dropoff"}}</label>
192
-
195
+ <InputGroup @name={{t "order.fields.dropoff"}} @required={{true}}>
196
+ <div class="flex items-center justify-end">
193
197
  <div class="flex-row space-x-2 pr-0.5">
194
198
  {{#if @resource.payload.dropoff}}
195
199
  <a href="javascript:;" {{on "click" (fn this.editPlace @resource.payload.dropoff)}} disaled={{cannot "fleet-ops update place"}}>
@@ -268,4 +272,4 @@
268
272
  </InputGroup>
269
273
  </div>
270
274
  {{/if}}
271
- </ContentPanel>
275
+ </ContentPanel>
@@ -4,7 +4,7 @@ import { inject as service } from '@ember/service';
4
4
  import { action, get } from '@ember/object';
5
5
  import { task } from 'ember-concurrency';
6
6
  import { colorForId, routeColorForStatus, routeStyleForStatus } from '../../../utils/route-colors';
7
- import { buildRoutePointMarkerPresentation, buildRoutePointsFromPayload } from '../../../utils/route-visualization';
7
+ import { buildRoutePointMarkerPresentation, buildRoutePointsFromPayload, describeRoutePoint } from '../../../utils/route-visualization';
8
8
 
9
9
  const ORDER_ROUTE_PREVIEW_PADDING_BOTTOM_RIGHT = [420, 0];
10
10
  const ORDER_ROUTE_PREVIEW_MAX_ZOOM_TWO_POINTS = 13;
@@ -45,6 +45,42 @@ export default class OrderFormRouteComponent extends Component {
45
45
  return buildRoutePointsFromPayload(this.args.resource.payload);
46
46
  }
47
47
 
48
+ get routeColor() {
49
+ const order = this.args.resource;
50
+
51
+ return colorForId(order?.public_id ?? order?.id ?? 'new-order');
52
+ }
53
+
54
+ get waypointRouteStops() {
55
+ const waypoints = this.args.resource.payload?.waypoints ?? [];
56
+
57
+ return waypoints.map((_waypoint, index) => {
58
+ const routePoint = {
59
+ role: 'waypoint',
60
+ stopNumber: index + 1,
61
+ };
62
+
63
+ return {
64
+ ...describeRoutePoint(routePoint, this.routeColor),
65
+ badgeStyle: this.badgeStyleForWaypoint(index),
66
+ required: this.isRequiredWaypoint(index),
67
+ };
68
+ });
69
+ }
70
+
71
+ badgeStyleForWaypoint(index) {
72
+ const { markerColor } = describeRoutePoint({ role: 'waypoint', stopNumber: index + 1 }, this.routeColor);
73
+ const normalizedColor = markerColor?.toLowerCase?.();
74
+ const isYellow = normalizedColor === '#facc15' || normalizedColor === '#ca8a04';
75
+ const textColor = isYellow ? '#111827' : '#ffffff';
76
+
77
+ return `background-color: ${markerColor}; color: ${textColor};`;
78
+ }
79
+
80
+ isRequiredWaypoint(index) {
81
+ return index < 2;
82
+ }
83
+
48
84
  willDestroy() {
49
85
  super.willDestroy(...arguments);
50
86
  if (this.routingControl) {
@@ -75,7 +75,7 @@
75
75
  {{#each serviceQuote.items as |item|}}
76
76
  <tr>
77
77
  <td>
78
- <span class="font-semibold">{{smart-humanize item.details}}</span>
78
+ <span class="font-semibold">{{item.details}}</span>
79
79
  </td>
80
80
  <td>
81
81
  <span>{{format-currency item.amount serviceQuote.currency}}</span>
@@ -106,4 +106,4 @@
106
106
  </div>
107
107
  {{/if}}
108
108
  </div>
109
- </ContentPanel>
109
+ </ContentPanel>
@@ -10,10 +10,13 @@
10
10
  />
11
11
  <Button @type="default" @icon="rotate" @text="Reset" @onClick={{this.resetConfig}} @disabled={{or this.config.core_service (cannot "fleet-ops update order-config")}} />
12
12
  </div>
13
- <div class="activity-flow-graph-container">
13
+ <div class="activity-flow-graph-container" {{did-insert this.setupGraphScrollContainer}}>
14
14
  <div class="activity-flow-zoom-controls">
15
15
  <Button @icon="plus" @onClick={{this.zoomIn}} />
16
16
  <Button @icon="minus" @onClick={{this.zoomOut}} />
17
17
  </div>
18
18
  <JointGraph @onSetup={{this.initializeGraph}} />
19
+ </div>
20
+ <div class="activity-flow-horizontal-scrollbar {{unless this.hasHorizontalOverflow 'is-hidden'}}" {{did-insert this.setupHorizontalScrollbar}}>
21
+ <div class="activity-flow-horizontal-scrollbar-content"></div>
19
22
  </div>
@@ -12,6 +12,7 @@ import generateUUID from '@fleetbase/ember-core/utils/generate-uuid';
12
12
  import inlineTask from '@fleetbase/ember-core/utils/inline-task';
13
13
  import createFlowActivity from '../../utils/create-flow-activity';
14
14
  import contextComponentCallback from '@fleetbase/ember-core/utils/context-component-callback';
15
+ import normalizeOrderConfigFlow, { getOrderConfigFlowRootCode } from '../../utils/normalize-order-config-flow';
15
16
 
16
17
  /**
17
18
  * Manages the activity flow for order configuration, allowing users to create, edit, and view a sequence of activities.
@@ -53,6 +54,13 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
53
54
  */
54
55
  @tracked graph;
55
56
 
57
+ /**
58
+ * True when the rendered graph is wider than the visible Activity Flow area.
59
+ *
60
+ * @type {Boolean}
61
+ */
62
+ @tracked hasHorizontalOverflow = false;
63
+
56
64
  /**
57
65
  * Checks if listener pointerdown listener is called.
58
66
  *
@@ -60,6 +68,14 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
60
68
  */
61
69
  @tracked listeningForClicks = false;
62
70
 
71
+ graphScrollContainer;
72
+ horizontalScrollbar;
73
+ horizontalScrollbarContent;
74
+ isSyncingHorizontalScroll = false;
75
+ onGraphScroll;
76
+ onHorizontalScrollbarScroll;
77
+ onWindowResize;
78
+
63
79
  /**
64
80
  * Initializes the component with the given configuration.
65
81
  * @param {Object} owner - The owner of the component instance.
@@ -85,6 +101,42 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
85
101
  this.initializeActivityJointModel();
86
102
  this.initializeActivityFlow();
87
103
  this.listenForElementClicks();
104
+ this.updateHorizontalScrollbar();
105
+ }
106
+
107
+ @action setupGraphScrollContainer(element) {
108
+ this.graphScrollContainer = element;
109
+ this.onGraphScroll = () => this.syncHorizontalScrollbarFromGraph();
110
+ this.onWindowResize = () => this.updateHorizontalScrollbar();
111
+
112
+ element.addEventListener('scroll', this.onGraphScroll);
113
+ window.addEventListener('resize', this.onWindowResize);
114
+ this.updateHorizontalScrollbar();
115
+ }
116
+
117
+ @action setupHorizontalScrollbar(element) {
118
+ this.horizontalScrollbar = element;
119
+ this.horizontalScrollbarContent = element.firstElementChild;
120
+ this.onHorizontalScrollbarScroll = () => this.syncGraphFromHorizontalScrollbar();
121
+
122
+ element.addEventListener('scroll', this.onHorizontalScrollbarScroll);
123
+ this.updateHorizontalScrollbar();
124
+ }
125
+
126
+ willDestroy() {
127
+ super.willDestroy(...arguments);
128
+
129
+ if (this.graphScrollContainer && this.onGraphScroll) {
130
+ this.graphScrollContainer.removeEventListener('scroll', this.onGraphScroll);
131
+ }
132
+
133
+ if (this.horizontalScrollbar && this.onHorizontalScrollbarScroll) {
134
+ this.horizontalScrollbar.removeEventListener('scroll', this.onHorizontalScrollbarScroll);
135
+ }
136
+
137
+ if (this.onWindowResize) {
138
+ window.removeEventListener('resize', this.onWindowResize);
139
+ }
88
140
  }
89
141
 
90
142
  /**
@@ -299,14 +351,20 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
299
351
  * @param {Object} incomingFlow - The incoming serialized activity flow.
300
352
  */
301
353
  deserializeFlow(incomingFlow) {
354
+ const normalizedFlow = normalizeOrderConfigFlow(incomingFlow);
302
355
  const deserializedFlow = {};
303
- const keys = Object.keys(incomingFlow);
356
+ const keys = Object.keys(normalizedFlow);
304
357
  keys.forEach((key) => {
305
- const activity = this.deserializeActivity(incomingFlow[key], incomingFlow);
306
- deserializedFlow[activity.get('code')] = activity;
358
+ const activity = this.deserializeActivity(normalizedFlow[key], normalizedFlow, deserializedFlow);
359
+ if (activity) {
360
+ deserializedFlow[activity.get('code')] = activity;
361
+ }
307
362
  });
308
363
 
309
- this.addDeserializedActivityToGraph(deserializedFlow.created);
364
+ const rootCode = getOrderConfigFlowRootCode(deserializedFlow);
365
+ if (rootCode && deserializedFlow[rootCode]) {
366
+ this.addDeserializedActivityToGraph(deserializedFlow[rootCode]);
367
+ }
310
368
  }
311
369
 
312
370
  /**
@@ -315,8 +373,21 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
315
373
  * @param {Object} incomingFlow - The entire incoming serialized activity flow for reference.
316
374
  * @returns {Object} Deserialized activity object.
317
375
  */
318
- deserializeActivity(activityObject, incomingFlow) {
319
- const activity = createFlowActivity(activityObject.code, activityObject.status, activityObject.details, activityObject.sequence, activityObject.color, {
376
+ deserializeActivity(activityObject, incomingFlow, deserializedFlow = {}) {
377
+ if (!activityObject || typeof activityObject !== 'object') {
378
+ return;
379
+ }
380
+
381
+ const code = activityObject.code ?? activityObject.key;
382
+ if (!code) {
383
+ return;
384
+ }
385
+
386
+ if (deserializedFlow[code]) {
387
+ return deserializedFlow[code];
388
+ }
389
+
390
+ const activity = createFlowActivity(code, activityObject.status, activityObject.details, activityObject.sequence, activityObject.color, {
320
391
  key: activityObject.key,
321
392
  logic: activityObject.logic ?? [],
322
393
  events: activityObject.events ?? [],
@@ -327,13 +398,14 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
327
398
  complete: activityObject.complete ?? false,
328
399
  internalId: activityObject.internalId ?? generateUUID(),
329
400
  });
401
+ deserializedFlow[code] = activity;
330
402
  const { activities } = activityObject;
331
403
  if (isArray(activities)) {
332
404
  activity.set(
333
405
  'activities',
334
406
  activities
335
407
  .map((activityKey) => {
336
- return this.deserializeActivity(incomingFlow[activityKey], incomingFlow);
408
+ return this.deserializeActivity(incomingFlow[activityKey], incomingFlow, deserializedFlow);
337
409
  })
338
410
  .filter(Boolean)
339
411
  );
@@ -347,6 +419,10 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
347
419
  * @param {Object} activity - The activity to add to the graph.
348
420
  */
349
421
  addDeserializedActivityToGraph(activity) {
422
+ if (!activity) {
423
+ return;
424
+ }
425
+
350
426
  const positionals = this.getActivityPositioning(activity);
351
427
  const parentActivity = this.createActivityNode(activity, positionals);
352
428
  const childActivities = activity.get('activities');
@@ -355,6 +431,7 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
355
431
  this.addActivityNodeTools(parentActivity, positionals);
356
432
  this.addChildActivities(parentActivity, childActivities);
357
433
  this.repositionAllActivities();
434
+ this.resizePaperWidthToFitActivities();
358
435
  }
359
436
 
360
437
  /**
@@ -394,6 +471,79 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
394
471
  });
395
472
  }
396
473
 
474
+ /**
475
+ * Expands the JointJS paper width so wide activity flows can scroll horizontally.
476
+ *
477
+ * @returns {void}
478
+ */
479
+ resizePaperWidthToFitActivities() {
480
+ if (!this.paper) {
481
+ return;
482
+ }
483
+
484
+ const nodes = Object.values(this.flow)
485
+ .map((activity) => activity.get('node'))
486
+ .filter(Boolean);
487
+
488
+ if (nodes.length === 0) {
489
+ return;
490
+ }
491
+
492
+ const padding = 150;
493
+ const viewportWidth = this.paper.el.parentElement?.clientWidth ?? this.paper.options.width;
494
+ const currentHeight = this.paper.options.height;
495
+ const contentWidth = nodes.reduce((width, node) => {
496
+ const position = node.position();
497
+ const size = node.size();
498
+
499
+ return Math.max(width, position.x + size.width + padding);
500
+ }, 0);
501
+ const paperWidth = Math.max(viewportWidth, contentWidth);
502
+
503
+ this.paper.setDimensions(paperWidth, currentHeight);
504
+ this.paper.el.style.width = contentWidth > viewportWidth ? `${paperWidth}px` : '100%';
505
+ this.paper.el.style.minWidth = '100%';
506
+ next(this, this.updateHorizontalScrollbar);
507
+ }
508
+
509
+ updateHorizontalScrollbar() {
510
+ if (!this.graphScrollContainer || !this.horizontalScrollbar || !this.horizontalScrollbarContent) {
511
+ return;
512
+ }
513
+
514
+ const scrollWidth = this.graphScrollContainer.scrollWidth;
515
+ const clientWidth = this.graphScrollContainer.clientWidth;
516
+ const hasOverflow = scrollWidth > clientWidth + 1;
517
+
518
+ this.hasHorizontalOverflow = hasOverflow;
519
+ this.horizontalScrollbarContent.style.width = hasOverflow ? `${scrollWidth}px` : '100%';
520
+ this.horizontalScrollbar.scrollLeft = this.graphScrollContainer.scrollLeft;
521
+
522
+ const { left, width } = this.graphScrollContainer.getBoundingClientRect();
523
+ this.horizontalScrollbar.style.left = `${left}px`;
524
+ this.horizontalScrollbar.style.width = `${width}px`;
525
+ }
526
+
527
+ syncHorizontalScrollbarFromGraph() {
528
+ if (this.isSyncingHorizontalScroll || !this.horizontalScrollbar || !this.graphScrollContainer) {
529
+ return;
530
+ }
531
+
532
+ this.isSyncingHorizontalScroll = true;
533
+ this.horizontalScrollbar.scrollLeft = this.graphScrollContainer.scrollLeft;
534
+ this.isSyncingHorizontalScroll = false;
535
+ }
536
+
537
+ syncGraphFromHorizontalScrollbar() {
538
+ if (this.isSyncingHorizontalScroll || !this.horizontalScrollbar || !this.graphScrollContainer) {
539
+ return;
540
+ }
541
+
542
+ this.isSyncingHorizontalScroll = true;
543
+ this.graphScrollContainer.scrollLeft = this.horizontalScrollbar.scrollLeft;
544
+ this.isSyncingHorizontalScroll = false;
545
+ }
546
+
397
547
  /**
398
548
  * Creates and adds a link between a parent activity and a child activity on the graph.
399
549
  *
@@ -484,6 +634,7 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
484
634
  const firstActivity = this.addActivityToGraph(activityObject);
485
635
  lastActivity = firstActivity;
486
636
  });
637
+ this.resizePaperWidthToFitActivities();
487
638
  return;
488
639
  }
489
640
 
@@ -550,6 +701,16 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
550
701
  return this.flow;
551
702
  }
552
703
 
704
+ /**
705
+ * Coerces activity label fields before passing them to JointJS text helpers.
706
+ *
707
+ * @param {*} value
708
+ * @returns {String}
709
+ */
710
+ activityText(value) {
711
+ return typeof value === 'string' ? value : value == null ? '' : String(value);
712
+ }
713
+
553
714
  /**
554
715
  * Creates a new activity node based on the provided activity and positioning data.
555
716
  * @param {Object} activity - The activity data.
@@ -558,14 +719,17 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
558
719
  */
559
720
  createActivityNode(activity, positionals = {}) {
560
721
  const { width, height, x, y } = positionals;
561
- const wrappedDetails = joint.util.breakText(activity.get('details'), { width });
722
+ const code = this.activityText(activity.get('code'));
723
+ const status = this.activityText(activity.get('status'));
724
+ const details = this.activityText(activity.get('details'));
725
+ const wrappedDetails = this.activityText(joint.util.breakText(details, { width }));
562
726
  const activityNode = new joint.shapes.fleetbase.Activity({
563
727
  position: { x, y },
564
728
  size: { width, height },
565
729
  attrs: {
566
730
  pill: {
567
731
  ref: 'code',
568
- refWidth: activity.get('code').length * 1.35,
732
+ refWidth: code.length * 1.35,
569
733
  refHeight: 5,
570
734
  refX: -5,
571
735
  refY: -2,
@@ -575,8 +739,8 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
575
739
  stroke: '#374151',
576
740
  strokeWidth: 1,
577
741
  },
578
- code: { ref: 'pill', text: activity.get('code'), fill: 'white', fontSize: 14, textAnchor: 'left', refX: 15, refY: 20, yAlignment: 'middle' },
579
- status: { text: activity.get('status'), fill: 'white', fontSize: 14, fontWeight: 'bold', refY: 40, refX: 10, textAnchor: 'left' },
742
+ code: { ref: 'pill', text: code, fill: 'white', fontSize: 14, textAnchor: 'left', refX: 15, refY: 20, yAlignment: 'middle' },
743
+ status: { text: status, fill: 'white', fontSize: 14, fontWeight: 'bold', refY: 40, refX: 10, textAnchor: 'left' },
580
744
  details: {
581
745
  text: wrappedDetails,
582
746
  fill: 'white',
@@ -608,7 +772,8 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
608
772
  const width = 250;
609
773
  const baseHeight = 90;
610
774
  const lineHeight = 10;
611
- const wrappedDetails = joint.util.breakText(activity.get('details'), { width });
775
+ const details = this.activityText(activity.get('details'));
776
+ const wrappedDetails = this.activityText(joint.util.breakText(details, { width }));
612
777
  const numberOfLines = wrappedDetails.split('\n').length;
613
778
  const height = baseHeight + lineHeight * (numberOfLines - 1);
614
779
  let x = 100;
@@ -933,6 +1098,7 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
933
1098
  target: { id: newActivity.get('id') },
934
1099
  });
935
1100
  link.addTo(this.graph);
1101
+ this.resizePaperWidthToFitActivities();
936
1102
 
937
1103
  return newActivity;
938
1104
  }