@fleetbase/fleetops-engine 0.6.56 → 0.6.58
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/ai/create-order-preview.hbs +297 -0
- package/addon/components/ai/create-order-preview.js +362 -0
- package/addon/components/ai/route-preview-map.hbs +79 -0
- package/addon/components/ai/route-preview-map.js +527 -0
- package/addon/components/fleet/details.hbs +21 -8
- package/addon/components/fleet/details.js +55 -1
- package/addon/components/fleet/hierarchy-tree.hbs +137 -0
- package/addon/components/fleet/hierarchy-tree.js +528 -0
- package/addon/components/map/drawer/device-event-listing.js +1 -0
- package/addon/components/map/drawer/driver-listing.js +1 -0
- package/addon/components/map/drawer/place-listing.js +1 -0
- package/addon/components/map/drawer/vehicle-listing.js +1 -0
- package/addon/components/order/details.hbs +34 -1
- package/addon/components/order/form/details.hbs +4 -0
- package/addon/components/order/form.hbs +9 -1
- package/addon/components/vehicle/card.hbs +14 -2
- package/addon/components/vehicle/form.hbs +13 -1
- package/addon/controllers/settings/map.js +4 -0
- package/addon/extension.js +14 -0
- package/addon/routes/management/fleets/index/details.js +1 -1
- package/addon/services/map-adapter/google.js +56 -16
- package/addon/services/map-manager.js +12 -0
- package/addon/styles/fleetops-engine.css +487 -1
- package/addon/utils/map-drawer-dropdown-position.js +11 -13
- package/app/components/ai/create-order-preview.js +1 -0
- package/app/components/ai/route-preview-map.js +1 -0
- package/app/components/fleet/hierarchy-tree.js +1 -0
- package/app/services/route-engine.js +1 -0
- package/composer.json +9 -2
- package/extension.json +1 -1
- package/package.json +6 -4
- package/phpunit.xml.dist +4 -4
- package/pnpm-workspace.yaml +2 -1
- package/scripts/coverage-runner.php +51 -0
- package/scripts/coverage-summary.php +110 -0
- package/scripts/normalize-rgba-alpha.js +90 -0
- package/scripts/pest-bootstrap.php +177 -0
- package/scripts/pest-file-runner.php +82 -0
- package/scripts/pest-runner.php +64 -0
- package/server/src/Http/Resources/v1/Fleet.php +8 -2
- package/server/src/Models/Driver.php +6 -0
- package/server/src/Models/Order.php +18 -6
- package/server/src/Models/Place.php +11 -3
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +3 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +26 -0
- package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +88 -0
- package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +108 -0
- package/server/src/Support/Ai/Capabilities/ConsoleNavigationCapability.php +55 -0
- package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +508 -0
- package/server/src/Support/Ai/Capabilities/DocsHelpCapability.php +49 -0
- package/server/src/Support/Ai/Capabilities/ImportOrdersPreviewCapability.php +66 -0
- package/server/src/Support/Ai/Capabilities/OperationalQueryCapability.php +319 -0
- package/server/src/Support/Ai/Capabilities/OptimizeOrderRouteCapability.php +240 -0
- package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +95 -0
- package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +195 -0
- package/server/src/Support/Ai/FleetOpsAiQueryResources.php +150 -0
- package/server/src/Support/Utils.php +52 -0
- package/server/src/Tracking/TrackingOptions.php +6 -1
- package/server/src/routes.php +0 -11
- package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
- package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
- package/server/tests/AnalyticsRoutesTest.php +5 -3
- package/server/tests/CustomerEndpointTest.php +1 -0
- package/server/tests/DeviceFilterTest.php +1 -2
- package/server/tests/FleetHierarchyResourceTest.php +12 -0
- package/server/tests/FuelProviderRegistryTest.php +7 -9
- package/server/tests/LiveControllerViewportTest.php +8 -16
- package/server/tests/LiveOrderQueryTest.php +18 -24
- package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
- package/server/tests/PointResolutionTest.php +63 -0
- package/server/tests/ServiceAreaZoneApiShapeTest.php +8 -9
- package/server/tests/ServiceRateAlgorithmWeightVariablesTest.php +1 -1
- package/server/tests/TelematicsHardeningTest.php +31 -26
- package/server/tests/TrackingIntelligenceTest.php +65 -25
- package/server/tests/VehicleDriverAssignmentTest.php +8 -9
- package/addon/components/table/cell/dropdown/action-item.hbs +0 -31
- package/addon/components/table/cell/dropdown/action-item.js +0 -62
- package/addon/components/table/cell/dropdown.hbs +0 -28
- package/addon/components/table/cell/dropdown.js +0 -91
- package/app/components/table/cell/dropdown/action-item.js +0 -1
- package/app/components/table/cell/dropdown.js +0 -1
- package/server/src/Http/Controllers/Api/v1/OrganizationController.php +0 -45
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
<div class="fleetbase-ai-response-region fleetbase-ai-create-order-preview" {{did-update this.updatePreview @preview}}>
|
|
2
|
+
<div class="fleetbase-ai-create-order-preview__header">
|
|
3
|
+
<div>
|
|
4
|
+
<div class="fleetbase-ai-action-title">{{this.preview.label}}</div>
|
|
5
|
+
<div class="fleetbase-ai-action-message">{{this.preview.message}}</div>
|
|
6
|
+
</div>
|
|
7
|
+
<span class="fleetbase-ai-action-status">{{if this.isCompleted "Completed" (if this.isCancelled "Cancelled" (if this.preview.ready "Ready" "Needs info"))}}</span>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
{{#if @isApplying}}
|
|
11
|
+
<div class="fleetbase-ai-create-order-preview__applying">
|
|
12
|
+
<span class="fleetbase-ai-create-order-preview__magic-loader" aria-hidden="true">
|
|
13
|
+
<span></span>
|
|
14
|
+
<span></span>
|
|
15
|
+
<span></span>
|
|
16
|
+
</span>
|
|
17
|
+
<span>Creating Fleet-Ops order...</span>
|
|
18
|
+
</div>
|
|
19
|
+
{{else if this.isCompleted}}
|
|
20
|
+
<div class="fleetbase-ai-create-order-preview__completed">
|
|
21
|
+
<div class="fleetbase-ai-create-order-preview__completed-icon">
|
|
22
|
+
<FaIcon @icon="check" />
|
|
23
|
+
</div>
|
|
24
|
+
<div class="fleetbase-ai-create-order-preview__completed-copy">
|
|
25
|
+
<strong>{{this.completedMessage}}</strong>
|
|
26
|
+
<span>{{this.orderReference}}</span>
|
|
27
|
+
</div>
|
|
28
|
+
{{#if this.canOpenOrder}}
|
|
29
|
+
<Button @size="sm" @icon="arrow-up-right-from-square" @text="Open order" @onClick={{this.openOrder}} />
|
|
30
|
+
{{/if}}
|
|
31
|
+
</div>
|
|
32
|
+
{{else if this.isCancelled}}
|
|
33
|
+
<div class="fleetbase-ai-create-order-preview__cancelled">
|
|
34
|
+
<FaIcon @icon="circle-xmark" />
|
|
35
|
+
<span>{{this.cancelledMessage}}</span>
|
|
36
|
+
</div>
|
|
37
|
+
{{else if this.preview.error}}
|
|
38
|
+
<div class="fleetbase-ai-action-error">{{this.preview.error.message}}</div>
|
|
39
|
+
{{else}}
|
|
40
|
+
<Ai::RoutePreviewMap @payload={{this.payload}} @orderId={{this.orderPreviewId}} @status="created" />
|
|
41
|
+
|
|
42
|
+
<div class="fleetbase-ai-create-order-preview__fields">
|
|
43
|
+
<div class="fleetbase-ai-create-order-preview__field-row">
|
|
44
|
+
<div class="fleetbase-ai-create-order-preview__field-copy">
|
|
45
|
+
<span>Order type</span>
|
|
46
|
+
{{#if this.isEditingOrderType}}
|
|
47
|
+
<ModelSelect
|
|
48
|
+
@modelName="order-config"
|
|
49
|
+
@selectedModel={{this.draft.order_config_uuid}}
|
|
50
|
+
@placeholder="Select order type"
|
|
51
|
+
@triggerClass="fleetbase-ai-create-order-preview__inline-select"
|
|
52
|
+
@infiniteScroll={{false}}
|
|
53
|
+
@renderInPlace={{true}}
|
|
54
|
+
@onChange={{this.setOrderConfig}}
|
|
55
|
+
as |model|
|
|
56
|
+
>
|
|
57
|
+
{{or model.name model.key}}
|
|
58
|
+
</ModelSelect>
|
|
59
|
+
{{else}}
|
|
60
|
+
<strong>{{this.orderTypeLabel}}</strong>
|
|
61
|
+
{{/if}}
|
|
62
|
+
</div>
|
|
63
|
+
<Button
|
|
64
|
+
@icon={{if this.isEditingOrderType "xmark" "pen"}}
|
|
65
|
+
@helpText={{if this.isEditingOrderType "Close editor" "Edit order type"}}
|
|
66
|
+
@wrapperClass="fleetbase-ai-create-order-preview__edit-wrapper"
|
|
67
|
+
class="fleetbase-ai-create-order-preview__edit-button"
|
|
68
|
+
@onClick={{if this.isEditingOrderType this.closeEditor (fn this.editField "orderType")}}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="fleetbase-ai-create-order-preview__field-row">
|
|
73
|
+
<div class="fleetbase-ai-create-order-preview__field-copy">
|
|
74
|
+
<span>Schedule</span>
|
|
75
|
+
{{#if this.isEditingSchedule}}
|
|
76
|
+
<DateTimeInput
|
|
77
|
+
class="fleetbase-ai-create-order-preview__inline-datetime"
|
|
78
|
+
@value={{this.draft.scheduled_at}}
|
|
79
|
+
@minDate={{format-date-fns (now) "yyyy-MM-dd"}}
|
|
80
|
+
@minTime={{format-date-fns (now) "HH:mm"}}
|
|
81
|
+
@onUpdate={{this.setScheduledAt}}
|
|
82
|
+
/>
|
|
83
|
+
{{else}}
|
|
84
|
+
<strong>
|
|
85
|
+
{{#if this.draft.scheduled_at}}
|
|
86
|
+
{{format-date-fns this.draft.scheduled_at "yyyy-MM-dd HH:mm" fallback=this.scheduledAtLabel}}
|
|
87
|
+
{{else}}
|
|
88
|
+
{{this.scheduledAtLabel}}
|
|
89
|
+
{{/if}}
|
|
90
|
+
</strong>
|
|
91
|
+
{{/if}}
|
|
92
|
+
</div>
|
|
93
|
+
<Button
|
|
94
|
+
@icon={{if this.isEditingSchedule "check" "pen"}}
|
|
95
|
+
@helpText={{if this.isEditingSchedule "Done" "Edit schedule"}}
|
|
96
|
+
@wrapperClass="fleetbase-ai-create-order-preview__edit-wrapper"
|
|
97
|
+
class="fleetbase-ai-create-order-preview__edit-button"
|
|
98
|
+
@onClick={{if this.isEditingSchedule this.closeEditor (fn this.editField "schedule")}}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="fleetbase-ai-create-order-preview__field-row">
|
|
103
|
+
<div class="fleetbase-ai-create-order-preview__field-copy">
|
|
104
|
+
<span>Pickup</span>
|
|
105
|
+
{{#if this.isEditingPickup}}
|
|
106
|
+
<ModelSelect
|
|
107
|
+
@modelName="place"
|
|
108
|
+
@selectedModel={{this.payload.pickup}}
|
|
109
|
+
@placeholder="Pickup address"
|
|
110
|
+
@triggerClass="fleetbase-ai-create-order-preview__inline-select"
|
|
111
|
+
@customSearchEndpoint="places/search"
|
|
112
|
+
@query={{hash geo=true}}
|
|
113
|
+
@infiniteScroll={{false}}
|
|
114
|
+
@renderInPlace={{true}}
|
|
115
|
+
@onChange={{fn this.setPayloadPlace "pickup"}}
|
|
116
|
+
as |place|
|
|
117
|
+
>
|
|
118
|
+
<div class="fleetbase-ai-create-order-preview__select-label">{{this.addressLabel place}}</div>
|
|
119
|
+
</ModelSelect>
|
|
120
|
+
{{else}}
|
|
121
|
+
<strong>{{this.pickupLabel}}</strong>
|
|
122
|
+
{{/if}}
|
|
123
|
+
</div>
|
|
124
|
+
<Button
|
|
125
|
+
@icon={{if this.isEditingPickup "xmark" "pen"}}
|
|
126
|
+
@helpText={{if this.isEditingPickup "Close editor" "Edit pickup"}}
|
|
127
|
+
@wrapperClass="fleetbase-ai-create-order-preview__edit-wrapper"
|
|
128
|
+
class="fleetbase-ai-create-order-preview__edit-button"
|
|
129
|
+
@onClick={{if this.isEditingPickup this.closeEditor (fn this.editField "pickup")}}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div class="fleetbase-ai-create-order-preview__field-row">
|
|
134
|
+
<div class="fleetbase-ai-create-order-preview__field-copy">
|
|
135
|
+
<span>Dropoff</span>
|
|
136
|
+
{{#if this.isEditingDropoff}}
|
|
137
|
+
<ModelSelect
|
|
138
|
+
@modelName="place"
|
|
139
|
+
@selectedModel={{this.payload.dropoff}}
|
|
140
|
+
@placeholder="Dropoff address"
|
|
141
|
+
@triggerClass="fleetbase-ai-create-order-preview__inline-select"
|
|
142
|
+
@customSearchEndpoint="places/search"
|
|
143
|
+
@query={{hash geo=true}}
|
|
144
|
+
@infiniteScroll={{false}}
|
|
145
|
+
@renderInPlace={{true}}
|
|
146
|
+
@onChange={{fn this.setPayloadPlace "dropoff"}}
|
|
147
|
+
as |place|
|
|
148
|
+
>
|
|
149
|
+
<div class="fleetbase-ai-create-order-preview__select-label">{{this.addressLabel place}}</div>
|
|
150
|
+
</ModelSelect>
|
|
151
|
+
{{else}}
|
|
152
|
+
<strong>{{this.dropoffLabel}}</strong>
|
|
153
|
+
{{/if}}
|
|
154
|
+
</div>
|
|
155
|
+
<Button
|
|
156
|
+
@icon={{if this.isEditingDropoff "xmark" "pen"}}
|
|
157
|
+
@helpText={{if this.isEditingDropoff "Close editor" "Edit dropoff"}}
|
|
158
|
+
@wrapperClass="fleetbase-ai-create-order-preview__edit-wrapper"
|
|
159
|
+
class="fleetbase-ai-create-order-preview__edit-button"
|
|
160
|
+
@onClick={{if this.isEditingDropoff this.closeEditor (fn this.editField "dropoff")}}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div class="fleetbase-ai-create-order-preview__field-row">
|
|
165
|
+
<div class="fleetbase-ai-create-order-preview__field-copy">
|
|
166
|
+
<span>Driver</span>
|
|
167
|
+
{{#if this.isEditingDriver}}
|
|
168
|
+
<ModelSelect
|
|
169
|
+
@modelName="driver"
|
|
170
|
+
@placeholder="Assign driver"
|
|
171
|
+
@triggerClass="fleetbase-ai-create-order-preview__inline-select"
|
|
172
|
+
@allowClear={{true}}
|
|
173
|
+
@infiniteScroll={{false}}
|
|
174
|
+
@renderInPlace={{true}}
|
|
175
|
+
@onChange={{this.setDriver}}
|
|
176
|
+
as |driver|
|
|
177
|
+
>
|
|
178
|
+
{{driver.name}}
|
|
179
|
+
</ModelSelect>
|
|
180
|
+
{{else}}
|
|
181
|
+
<strong>{{this.driverLabel}}</strong>
|
|
182
|
+
{{/if}}
|
|
183
|
+
</div>
|
|
184
|
+
<Button
|
|
185
|
+
@icon={{if this.isEditingDriver "xmark" "pen"}}
|
|
186
|
+
@helpText={{if this.isEditingDriver "Close editor" "Edit driver"}}
|
|
187
|
+
@wrapperClass="fleetbase-ai-create-order-preview__edit-wrapper"
|
|
188
|
+
class="fleetbase-ai-create-order-preview__edit-button"
|
|
189
|
+
@onClick={{if this.isEditingDriver this.closeEditor (fn this.editField "driver")}}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="fleetbase-ai-create-order-preview__field-row">
|
|
194
|
+
<div class="fleetbase-ai-create-order-preview__field-copy">
|
|
195
|
+
<span>Vehicle</span>
|
|
196
|
+
{{#if this.isEditingVehicle}}
|
|
197
|
+
<ModelSelect
|
|
198
|
+
@modelName="vehicle"
|
|
199
|
+
@placeholder="Assign vehicle"
|
|
200
|
+
@triggerClass="fleetbase-ai-create-order-preview__inline-select"
|
|
201
|
+
@allowClear={{true}}
|
|
202
|
+
@infiniteScroll={{false}}
|
|
203
|
+
@renderInPlace={{true}}
|
|
204
|
+
@onChange={{this.setVehicle}}
|
|
205
|
+
as |vehicle|
|
|
206
|
+
>
|
|
207
|
+
{{or vehicle.display_name vehicle.name vehicle.plate_number}}
|
|
208
|
+
</ModelSelect>
|
|
209
|
+
{{else}}
|
|
210
|
+
<strong>{{this.vehicleLabel}}</strong>
|
|
211
|
+
{{/if}}
|
|
212
|
+
</div>
|
|
213
|
+
<Button
|
|
214
|
+
@icon={{if this.isEditingVehicle "xmark" "pen"}}
|
|
215
|
+
@helpText={{if this.isEditingVehicle "Close editor" "Edit vehicle"}}
|
|
216
|
+
@wrapperClass="fleetbase-ai-create-order-preview__edit-wrapper"
|
|
217
|
+
class="fleetbase-ai-create-order-preview__edit-button"
|
|
218
|
+
@onClick={{if this.isEditingVehicle this.closeEditor (fn this.editField "vehicle")}}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div class="fleetbase-ai-create-order-preview__field-row fleetbase-ai-create-order-preview__field-row--notes">
|
|
223
|
+
<div class="fleetbase-ai-create-order-preview__field-copy">
|
|
224
|
+
<span>Notes</span>
|
|
225
|
+
{{#if this.isEditingNotes}}
|
|
226
|
+
<Textarea
|
|
227
|
+
@value={{this.draft.notes}}
|
|
228
|
+
class="fleetbase-ai-create-order-preview__inline-textarea"
|
|
229
|
+
rows={{2}}
|
|
230
|
+
placeholder="Add order notes"
|
|
231
|
+
{{on "input" this.setNotes}}
|
|
232
|
+
/>
|
|
233
|
+
{{else}}
|
|
234
|
+
<strong>{{this.notesLabel}}</strong>
|
|
235
|
+
{{/if}}
|
|
236
|
+
</div>
|
|
237
|
+
<Button
|
|
238
|
+
@icon={{if this.isEditingNotes "check" "pen"}}
|
|
239
|
+
@helpText={{if this.isEditingNotes "Done" "Edit notes"}}
|
|
240
|
+
@wrapperClass="fleetbase-ai-create-order-preview__edit-wrapper"
|
|
241
|
+
class="fleetbase-ai-create-order-preview__edit-button"
|
|
242
|
+
@onClick={{if this.isEditingNotes this.closeEditor (fn this.editField "notes")}}
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div class="fleetbase-ai-create-order-preview__toggles">
|
|
248
|
+
<div class="fleetbase-ai-create-order-preview__toggle-row">
|
|
249
|
+
<Toggle @isToggled={{this.draft.pod_required}} @onToggle={{this.setPodRequired}} @label="Require POD" />
|
|
250
|
+
{{#if this.draft.pod_required}}
|
|
251
|
+
{{#if this.isEditingPodMethod}}
|
|
252
|
+
<select class="fleetbase-ai-create-order-preview__inline-native-select" value={{this.draft.pod_method}} {{on "change" this.setPodMethodFromEvent}}>
|
|
253
|
+
{{#each this.podMethods as |method|}}
|
|
254
|
+
<option value={{method}} selected={{eq method this.draft.pod_method}}>{{this.titleize method}}</option>
|
|
255
|
+
{{/each}}
|
|
256
|
+
</select>
|
|
257
|
+
{{else}}
|
|
258
|
+
<button type="button" class="fleetbase-ai-create-order-preview__inline-pill" {{on "click" (fn this.editField "podMethod")}}>
|
|
259
|
+
{{this.podMethodLabel}}
|
|
260
|
+
<FaIcon @icon="pen" />
|
|
261
|
+
</button>
|
|
262
|
+
{{/if}}
|
|
263
|
+
{{/if}}
|
|
264
|
+
</div>
|
|
265
|
+
<div class="fleetbase-ai-create-order-preview__toggle-row">
|
|
266
|
+
<Toggle @isToggled={{this.draft.dispatched}} @onToggle={{this.setDispatched}} @label="Dispatch after create" />
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{{#if this.hasMissingFields}}
|
|
271
|
+
<div class="fleetbase-ai-action-list">
|
|
272
|
+
<div class="fleetbase-ai-action-list-title">Required before apply</div>
|
|
273
|
+
<ul>
|
|
274
|
+
{{#each this.missingFields as |field|}}
|
|
275
|
+
<li>{{field}}</li>
|
|
276
|
+
{{/each}}
|
|
277
|
+
</ul>
|
|
278
|
+
</div>
|
|
279
|
+
{{/if}}
|
|
280
|
+
|
|
281
|
+
<div class="fleetbase-ai-create-order-preview__controls">
|
|
282
|
+
<Button
|
|
283
|
+
@type="primary"
|
|
284
|
+
@size="sm"
|
|
285
|
+
@icon="check"
|
|
286
|
+
@text="Create order"
|
|
287
|
+
@disabled={{this.isApplyDisabled}}
|
|
288
|
+
@isLoading={{@isApplying}}
|
|
289
|
+
@onClick={{this.apply}}
|
|
290
|
+
/>
|
|
291
|
+
<Button @size="sm" @icon="xmark" @text="Cancel" @disabled={{@isCancelling}} @onClick={{this.cancel}} />
|
|
292
|
+
{{#if this.refreshPreview.isRunning}}
|
|
293
|
+
<span class="fleetbase-ai-create-order-preview__refreshing">Updating preview...</span>
|
|
294
|
+
{{/if}}
|
|
295
|
+
</div>
|
|
296
|
+
{{/if}}
|
|
297
|
+
</div>
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
import { action, get } from '@ember/object';
|
|
5
|
+
import { task, timeout } from 'ember-concurrency';
|
|
6
|
+
|
|
7
|
+
export default class AiCreateOrderPreviewComponent extends Component {
|
|
8
|
+
@service router;
|
|
9
|
+
|
|
10
|
+
@tracked draft = this.clone(this.args.preview?.draft ?? {});
|
|
11
|
+
@tracked preview = this.args.preview;
|
|
12
|
+
@tracked editingField = null;
|
|
13
|
+
|
|
14
|
+
get payload() {
|
|
15
|
+
return this.draft.payload ?? {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get routeStops() {
|
|
19
|
+
return this.preview?.route_preview?.stops ?? [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get podMethods() {
|
|
23
|
+
return this.preview?.options?.pod_methods ?? ['scan', 'signature', 'photo'];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get isReady() {
|
|
27
|
+
return this.preview?.ready === true && !this.isTerminal;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get isCompleted() {
|
|
31
|
+
return Boolean(this.preview?.result);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get isTerminal() {
|
|
35
|
+
return this.isCompleted || this.isCancelled || Boolean(this.preview?.error);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get isCancelled() {
|
|
39
|
+
return this.args.task?.status === 'cancelled' || this.preview?.error?.type === 'cancelled';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get cancelledMessage() {
|
|
43
|
+
return this.preview?.error?.message ?? 'This order creation preview was cancelled.';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get completedMessage() {
|
|
47
|
+
return this.preview?.result?.message ?? 'Fleet-Ops order was created.';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get resultResource() {
|
|
51
|
+
return this.preview?.result?.resource;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get orderReference() {
|
|
55
|
+
return this.resultResource?.id ?? this.resultResource?.uuid ?? 'Created order';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get canOpenOrder() {
|
|
59
|
+
return Boolean(this.resultResource?.route && (this.resultResource?.models?.length || this.resultResource?.uuid || this.resultResource?.id));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get missingFields() {
|
|
63
|
+
return this.preview?.missing_fields ?? [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get hasMissingFields() {
|
|
67
|
+
return this.missingFields.length > 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get orderPreviewId() {
|
|
71
|
+
return this.args.task?.uuid ?? this.args.task?.id ?? this.preview?.key ?? 'ai-order-preview';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get orderTypeLabel() {
|
|
75
|
+
return this.titleize(this.draft.type ?? this.draft.order_config_name ?? this.draft.order_config_uuid) ?? 'Select order type';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get scheduledAtLabel() {
|
|
79
|
+
const scheduledAt = this.draft.scheduled_at;
|
|
80
|
+
|
|
81
|
+
if (!scheduledAt) {
|
|
82
|
+
return 'Add schedule';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return String(scheduledAt);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get pickupLabel() {
|
|
89
|
+
return this.addressLabel(this.payload.pickup) ?? this.payload.pickup_query ?? 'Add pickup';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get dropoffLabel() {
|
|
93
|
+
return this.addressLabel(this.payload.dropoff) ?? this.payload.dropoff_query ?? 'Add dropoff';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get driverLabel() {
|
|
97
|
+
return this.draft.driver_query ?? this.draft.driver_name ?? 'Assign driver';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get vehicleLabel() {
|
|
101
|
+
return this.draft.vehicle_query ?? this.draft.vehicle_name ?? 'Assign vehicle';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get notesLabel() {
|
|
105
|
+
return this.draft.notes?.trim?.() || 'Add order notes';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get podMethodLabel() {
|
|
109
|
+
return this.titleize(this.draft.pod_method ?? 'scan');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get isEditingOrderType() {
|
|
113
|
+
return this.editingField === 'orderType';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get isEditingSchedule() {
|
|
117
|
+
return this.editingField === 'schedule';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get isEditingPickup() {
|
|
121
|
+
return this.editingField === 'pickup';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get isEditingDropoff() {
|
|
125
|
+
return this.editingField === 'dropoff';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get isEditingDriver() {
|
|
129
|
+
return this.editingField === 'driver';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get isEditingVehicle() {
|
|
133
|
+
return this.editingField === 'vehicle';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get isEditingPodMethod() {
|
|
137
|
+
return this.editingField === 'podMethod';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get isEditingNotes() {
|
|
141
|
+
return this.editingField === 'notes';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get isApplyDisabled() {
|
|
145
|
+
return !this.isReady || this.args.isApplying || this.refreshPreview.isRunning;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
addressLabel(place) {
|
|
149
|
+
if (!place) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const values = [place.address, place.street1, place.name, place.query, place.city, place.postal_code, place.country]
|
|
154
|
+
.map((value) => (value === null || value === undefined ? null : String(value).trim()))
|
|
155
|
+
.filter((value) => value && !['undefined', 'null'].includes(value.toLowerCase()));
|
|
156
|
+
|
|
157
|
+
return [...new Set(values)].slice(0, 3).join(' - ');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
clone(value) {
|
|
161
|
+
return JSON.parse(JSON.stringify(value ?? {}));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
modelValue(model, key) {
|
|
165
|
+
if (!model) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (typeof model.get === 'function') {
|
|
170
|
+
return model.get(key);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return get(model, key) ?? model[key];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
serializeModel(model, fallback = {}) {
|
|
177
|
+
if (!model) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (typeof model.toJSON === 'function') {
|
|
182
|
+
return {
|
|
183
|
+
...model.toJSON(),
|
|
184
|
+
id: this.modelValue(model, 'id'),
|
|
185
|
+
uuid: this.modelValue(model, 'uuid') ?? this.modelValue(model, 'id'),
|
|
186
|
+
public_id: this.modelValue(model, 'public_id'),
|
|
187
|
+
name: this.modelValue(model, 'name'),
|
|
188
|
+
address: this.addressLabel(model) ?? this.modelValue(model, 'address'),
|
|
189
|
+
street1: this.modelValue(model, 'street1'),
|
|
190
|
+
city: this.modelValue(model, 'city'),
|
|
191
|
+
postal_code: this.modelValue(model, 'postal_code'),
|
|
192
|
+
country: this.modelValue(model, 'country'),
|
|
193
|
+
latitude: this.modelValue(model, 'latitude'),
|
|
194
|
+
longitude: this.modelValue(model, 'longitude'),
|
|
195
|
+
...fallback,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { ...model, ...fallback };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
titleize(value) {
|
|
203
|
+
if (!value) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return String(value)
|
|
208
|
+
.replace(/[_-]+/g, ' ')
|
|
209
|
+
.replace(/\s+/g, ' ')
|
|
210
|
+
.trim()
|
|
211
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
updateDraft(updates = {}) {
|
|
215
|
+
this.draft = {
|
|
216
|
+
...this.draft,
|
|
217
|
+
...updates,
|
|
218
|
+
payload: {
|
|
219
|
+
...(this.draft.payload ?? {}),
|
|
220
|
+
...(updates.payload ?? {}),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
updateRouteDraft(updates = {}) {
|
|
226
|
+
this.updateDraft(updates);
|
|
227
|
+
this.refreshPreview.perform();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@action editField(field) {
|
|
231
|
+
if (this.isTerminal) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.editingField = field;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@action closeEditor() {
|
|
239
|
+
this.editingField = null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@task *refreshPreview() {
|
|
243
|
+
yield timeout(180);
|
|
244
|
+
const refreshed = yield this.args.onRefresh?.(this.args.task, this.preview, { draft: this.draft });
|
|
245
|
+
if (refreshed) {
|
|
246
|
+
this.preview = {
|
|
247
|
+
...this.preview,
|
|
248
|
+
...refreshed,
|
|
249
|
+
draft: this.draft,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@action setPayloadPlace(role, place) {
|
|
255
|
+
const serialized = this.serializeModel(place);
|
|
256
|
+
const payload = { ...(this.draft.payload ?? {}) };
|
|
257
|
+
|
|
258
|
+
payload[role] = serialized;
|
|
259
|
+
payload[`${role}_query`] = this.addressLabel(serialized) ?? serialized?.address ?? serialized?.name ?? null;
|
|
260
|
+
|
|
261
|
+
if (serialized?.uuid) {
|
|
262
|
+
payload[`${role}_uuid`] = serialized.uuid;
|
|
263
|
+
} else {
|
|
264
|
+
delete payload[`${role}_uuid`];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.updateRouteDraft({ payload });
|
|
268
|
+
this.closeEditor();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@action setOrderConfig(orderConfig) {
|
|
272
|
+
this.updateDraft({
|
|
273
|
+
order_config_uuid: this.modelValue(orderConfig, 'uuid') ?? this.modelValue(orderConfig, 'id'),
|
|
274
|
+
order_config_name: this.modelValue(orderConfig, 'name'),
|
|
275
|
+
type: this.modelValue(orderConfig, 'key'),
|
|
276
|
+
});
|
|
277
|
+
this.closeEditor();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@action setScheduledAt(value) {
|
|
281
|
+
this.updateDraft({ scheduled_at: value });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@action setDriver(driver) {
|
|
285
|
+
this.updateDraft({
|
|
286
|
+
driver: this.modelValue(driver, 'uuid') ?? this.modelValue(driver, 'id'),
|
|
287
|
+
driver_query: this.modelValue(driver, 'name') ?? this.modelValue(driver, 'public_id'),
|
|
288
|
+
});
|
|
289
|
+
this.closeEditor();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
@action setVehicle(vehicle) {
|
|
293
|
+
this.updateDraft({
|
|
294
|
+
vehicle_assigned_uuid: this.modelValue(vehicle, 'uuid') ?? this.modelValue(vehicle, 'id'),
|
|
295
|
+
vehicle_query: this.modelValue(vehicle, 'display_name') ?? this.modelValue(vehicle, 'name') ?? this.modelValue(vehicle, 'plate_number'),
|
|
296
|
+
});
|
|
297
|
+
this.closeEditor();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@action setPodRequired(value) {
|
|
301
|
+
this.updateDraft({
|
|
302
|
+
pod_required: value,
|
|
303
|
+
pod_method: value ? (this.draft.pod_method ?? 'scan') : null,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@action setPodMethod(value) {
|
|
308
|
+
this.updateDraft({ pod_method: value });
|
|
309
|
+
this.closeEditor();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@action setPodMethodFromEvent(event) {
|
|
313
|
+
this.setPodMethod(event.target.value);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@action setDispatched(value) {
|
|
317
|
+
this.updateDraft({ dispatched: value });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@action setNotes(valueOrEvent) {
|
|
321
|
+
const value = valueOrEvent?.target ? valueOrEvent.target.value : valueOrEvent;
|
|
322
|
+
|
|
323
|
+
this.updateDraft({ notes: value });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@action apply() {
|
|
327
|
+
if (this.isTerminal) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return this.args.onApply?.(this.args.task, this.preview, { draft: this.draft });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
@action cancel() {
|
|
335
|
+
if (this.isTerminal) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return this.args.onCancel?.(this.args.task);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@action openOrder() {
|
|
343
|
+
if (!this.canOpenOrder) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const models = this.resultResource.models?.length ? this.resultResource.models : [this.resultResource.uuid ?? this.resultResource.id].filter(Boolean);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
this.router.transitionTo(this.resultResource.route, ...models);
|
|
351
|
+
} catch {
|
|
352
|
+
this.router.transitionTo('operations.orders.index.details', ...models);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@action updatePreview(preview) {
|
|
357
|
+
this.preview = preview;
|
|
358
|
+
if (preview?.draft && !this.editingField) {
|
|
359
|
+
this.draft = this.clone(preview.draft);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|