@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.
- package/addon/components/customer/create-order-form.hbs +2 -2
- package/addon/components/customer/form.hbs +2 -2
- package/addon/components/customer/order-form.hbs +2 -2
- package/addon/components/customer/orders.hbs +2 -2
- package/addon/components/display-place.hbs +2 -2
- package/addon/components/map/google-live-map.js +6 -2
- package/addon/components/map/leaflet-live-map.hbs +48 -14
- package/addon/components/map/leaflet-live-map.js +184 -20
- package/addon/components/modals/order-change-destination.hbs +46 -0
- package/addon/components/modals/order-change-destination.js +106 -0
- package/addon/components/modals/stop-activity.hbs +28 -0
- package/addon/components/modals/stop-activity.js +52 -0
- package/addon/components/modals/update-order-activity.hbs +45 -3
- package/addon/components/modals/update-order-activity.js +143 -5
- package/addon/components/orchestrator/resource-panel.hbs +0 -10
- package/addon/components/orchestrator/resource-panel.js +2 -4
- package/addon/components/order/activity-list.hbs +2 -2
- package/addon/components/order/activity-timeline.hbs +31 -29
- package/addon/components/order/details/activity.hbs +3 -3
- package/addon/components/order/details/activity.js +11 -0
- package/addon/components/order/details/documents.js +2 -0
- package/addon/components/order/details/purchase-rate.hbs +2 -2
- package/addon/components/order/details/route.hbs +11 -3
- package/addon/components/order/details/route.js +91 -14
- package/addon/components/order/details/tracking.hbs +101 -72
- package/addon/components/order/details/tracking.js +149 -2
- package/addon/components/order/form/details.hbs +3 -3
- package/addon/components/order/form/documents.js +11 -4
- package/addon/components/order/form/route.hbs +108 -104
- package/addon/components/order/form/route.js +37 -1
- package/addon/components/order/form/service-rate.hbs +2 -2
- package/addon/components/order-config-manager/activity-flow.hbs +4 -1
- package/addon/components/order-config-manager/activity-flow.js +178 -12
- package/addon/components/place/address.js +1 -1
- package/addon/components/route-list.hbs +17 -17
- package/addon/components/route-list.js +42 -10
- package/addon/components/service-rate/details.hbs +41 -0
- package/addon/components/service-rate/form.hbs +118 -0
- package/addon/components/service-rate/form.js +55 -0
- package/addon/components/vehicle/form.js +1 -1
- package/addon/components/vehicle/pill.hbs +1 -1
- package/addon/controllers/management/vehicles/index/new.js +1 -1
- package/addon/controllers/operations/orders/index/details.js +4 -4
- package/addon/controllers/operations/orders/index.js +14 -0
- package/addon/helpers/place-address-html.js +2 -2
- package/addon/routes/operations/service-rates/index.js +1 -1
- package/addon/services/geofence.js +163 -19
- package/addon/services/leaflet-contextmenu-manager.js +27 -0
- package/addon/services/vehicle-actions.js +1 -1
- package/addon/styles/fleetops-engine.css +91 -1
- package/addon/utils/fleet-ops-options.js +2 -1
- package/addon/utils/normalize-order-config-flow.js +41 -0
- package/addon/utils/place-address-html.js +3 -2
- package/addon/utils/route-visualization.js +54 -7
- package/app/components/modals/order-change-destination.js +1 -0
- package/app/components/modals/stop-activity.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
- package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
- package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
- package/server/resources/views/labels/default.php +57 -49
- package/server/resources/views/labels/waypoint-label.php +27 -20
- package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
- package/server/seeders/Testing/FleetSeeder.php +22 -22
- package/server/seeders/Testing/OrdersSeeder.php +6 -22
- package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
- package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
- package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
- package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +429 -26
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
- package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
- package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
- package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
- package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
- package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
- package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
- package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
- package/server/src/Http/Resources/v1/Customer.php +79 -0
- package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
- package/server/src/Http/Resources/v1/Payload.php +23 -20
- package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
- package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
- package/server/src/Listeners/HandleOrderDispatched.php +2 -2
- package/server/src/Models/Contact.php +92 -15
- package/server/src/Models/Customer.php +64 -0
- package/server/src/Models/Driver.php +12 -3
- package/server/src/Models/Entity.php +2 -2
- package/server/src/Models/Order.php +29 -4
- package/server/src/Models/Payload.php +17 -1
- package/server/src/Models/ServiceRate.php +273 -1
- package/server/src/Models/ServiceRateFee.php +39 -9
- package/server/src/Models/TrackingNumber.php +8 -2
- package/server/src/Models/Vehicle.php +18 -2
- package/server/src/Models/Waypoint.php +2 -2
- package/server/src/Notifications/LateDeparture.php +2 -2
- package/server/src/Notifications/ProlongedStoppage.php +2 -2
- package/server/src/Notifications/RouteDeviation.php +2 -2
- package/server/src/Observers/ContactObserver.php +2 -0
- package/server/src/Observers/ServiceRateObserver.php +6 -6
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
- package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
- package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
- package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
- package/server/src/Support/Analytics/RevenueTrend.php +0 -1
- package/server/src/Support/Analytics/TopDrivers.php +1 -1
- package/server/src/Support/CustomerAuth.php +98 -0
- package/server/src/Support/LabelPdf.php +52 -0
- package/server/src/Support/Metrics/AbstractMetric.php +4 -4
- package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
- package/server/src/Tracking/TrackingContextBuilder.php +41 -38
- package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
- package/server/src/Tracking/TrackingStop.php +13 -11
- package/server/src/routes.php +43 -9
- package/server/tests/CapacityAllocationEngineTest.php +10 -10
- package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
- package/server/tests/CustomerEndpointTest.php +199 -0
- package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
- package/server/tests/LabelPdfRenderingTest.php +23 -0
- package/server/tests/LiveControllerViewportTest.php +60 -0
- package/server/tests/MetricsRegistryTest.php +4 -8
- package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
- package/server/tests/OrderConfigEndpointTest.php +86 -0
- package/server/tests/OrderFileAttachmentTest.php +20 -0
- package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
- package/server/tests/VehicleDriverAssignmentTest.php +18 -0
- package/translations/en-us.yaml +1 -0
- 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
|
-
|
|
38
|
-
<div
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<div class="flex
|
|
47
|
-
<div class="
|
|
48
|
-
<
|
|
49
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
</
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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">{{
|
|
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(
|
|
356
|
+
const keys = Object.keys(normalizedFlow);
|
|
304
357
|
keys.forEach((key) => {
|
|
305
|
-
const activity = this.deserializeActivity(
|
|
306
|
-
|
|
358
|
+
const activity = this.deserializeActivity(normalizedFlow[key], normalizedFlow, deserializedFlow);
|
|
359
|
+
if (activity) {
|
|
360
|
+
deserializedFlow[activity.get('code')] = activity;
|
|
361
|
+
}
|
|
307
362
|
});
|
|
308
363
|
|
|
309
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
579
|
-
status: { text:
|
|
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
|
|
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
|
}
|