@fleetbase/fleetops-engine 0.6.55 → 0.6.57

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 (161) hide show
  1. package/addon/components/admin/map-settings.hbs +1 -1
  2. package/addon/components/ai/create-order-preview.hbs +297 -0
  3. package/addon/components/ai/create-order-preview.js +362 -0
  4. package/addon/components/ai/route-preview-map.hbs +79 -0
  5. package/addon/components/ai/route-preview-map.js +527 -0
  6. package/addon/components/device/details.js +1 -1
  7. package/addon/components/device-event/details.hbs +192 -0
  8. package/addon/components/device-event/details.js +151 -0
  9. package/addon/components/driver/schedule.js +1 -1
  10. package/addon/components/fleet/details.hbs +21 -8
  11. package/addon/components/fleet/details.js +55 -1
  12. package/addon/components/fleet/hierarchy-tree.hbs +137 -0
  13. package/addon/components/fleet/hierarchy-tree.js +528 -0
  14. package/addon/components/layout/fleet-ops-sidebar.js +15 -9
  15. package/addon/components/map/drawer/device-event-listing.js +1 -0
  16. package/addon/components/map/drawer/driver-listing.js +1 -0
  17. package/addon/components/map/drawer/place-listing.js +1 -0
  18. package/addon/components/map/drawer/vehicle-listing.js +1 -0
  19. package/addon/components/order/details.hbs +34 -1
  20. package/addon/components/order/form/details.hbs +4 -0
  21. package/addon/components/order/form.hbs +9 -1
  22. package/addon/components/telematic/details.hbs +1 -1
  23. package/addon/components/telematic/form.hbs +2 -2
  24. package/addon/components/telematic/settings.hbs +1 -1
  25. package/addon/components/vehicle/card.hbs +14 -2
  26. package/addon/components/vehicle/form.hbs +13 -1
  27. package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
  28. package/addon/controllers/connectivity/devices/index/details/vehicle.js +2 -2
  29. package/addon/controllers/connectivity/events/details.js +68 -1
  30. package/addon/controllers/connectivity/sensors/index.js +4 -0
  31. package/addon/controllers/connectivity/telematics/details/events.js +1 -1
  32. package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
  33. package/addon/controllers/settings/map.js +19 -0
  34. package/addon/extension.js +14 -0
  35. package/addon/instance-initializers/register-vroom-allocation.js +1 -1
  36. package/addon/routes/connectivity/events/details.js +26 -1
  37. package/addon/routes/management/fleets/index/details.js +1 -1
  38. package/addon/routes/management/vehicles/index/details.js +3 -1
  39. package/addon/services/map-adapter/google.js +66 -1
  40. package/addon/services/map-adapter/leaflet.js +1 -1
  41. package/addon/services/map-adapter-interface.js +1 -1
  42. package/addon/services/map-manager.js +16 -1
  43. package/addon/services/map-settings.js +15 -0
  44. package/addon/styles/fleetops-engine.css +487 -1
  45. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -1
  46. package/addon/templates/connectivity/events/details.hbs +40 -1
  47. package/addon/templates/settings/map.hbs +38 -9
  48. package/addon/utils/leaflet-plugin-loader.js +5 -5
  49. package/addon/utils/map-drawer-dropdown-position.js +11 -13
  50. package/addon/utils/route-colors.js +1 -1
  51. package/app/components/ai/create-order-preview.js +1 -0
  52. package/app/components/ai/route-preview-map.js +1 -0
  53. package/app/components/device-event/details.hbs +192 -0
  54. package/app/components/device-event/details.js +1 -0
  55. package/app/components/fleet/hierarchy-tree.js +1 -0
  56. package/app/services/route-engine.js +1 -0
  57. package/composer.json +1 -1
  58. package/extension.json +1 -1
  59. package/package.json +6 -4
  60. package/scripts/normalize-rgba-alpha.js +90 -0
  61. package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
  62. package/server/src/Exports/ContactExport.php +12 -5
  63. package/server/src/Exports/DeviceExport.php +92 -0
  64. package/server/src/Exports/DriverExport.php +28 -3
  65. package/server/src/Exports/EquipmentExport.php +94 -0
  66. package/server/src/Exports/FleetExport.php +22 -7
  67. package/server/src/Exports/FuelReportExport.php +16 -1
  68. package/server/src/Exports/IssueExport.php +25 -3
  69. package/server/src/Exports/MaintenanceExport.php +106 -0
  70. package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
  71. package/server/src/Exports/OrderExport.php +24 -5
  72. package/server/src/Exports/PartExport.php +99 -0
  73. package/server/src/Exports/PlaceExport.php +33 -7
  74. package/server/src/Exports/SensorExport.php +94 -0
  75. package/server/src/Exports/ServiceAreaExport.php +31 -7
  76. package/server/src/Exports/ServiceRateExport.php +41 -1
  77. package/server/src/Exports/TelematicExport.php +90 -0
  78. package/server/src/Exports/VehicleExport.php +274 -8
  79. package/server/src/Exports/VendorExport.php +15 -2
  80. package/server/src/Exports/WorkOrderExport.php +92 -0
  81. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
  82. package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
  83. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
  84. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
  85. package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
  86. package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
  87. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
  88. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -0
  89. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
  90. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
  91. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
  92. package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
  93. package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
  94. package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
  95. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
  96. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
  97. package/server/src/Http/Filter/DeviceFilter.php +33 -10
  98. package/server/src/Http/Filter/EquipmentFilter.php +23 -0
  99. package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
  100. package/server/src/Http/Filter/PartFilter.php +47 -0
  101. package/server/src/Http/Filter/SensorFilter.php +34 -6
  102. package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
  103. package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
  104. package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
  105. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
  106. package/server/src/Http/Requests/CreatePartRequest.php +40 -0
  107. package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
  108. package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
  109. package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
  110. package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
  111. package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
  112. package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
  113. package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
  114. package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
  115. package/server/src/Http/Resources/v1/Device.php +72 -0
  116. package/server/src/Http/Resources/v1/Equipment.php +57 -0
  117. package/server/src/Http/Resources/v1/Fleet.php +8 -2
  118. package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
  119. package/server/src/Http/Resources/v1/Part.php +64 -0
  120. package/server/src/Http/Resources/v1/Sensor.php +68 -0
  121. package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
  122. package/server/src/Models/Driver.php +6 -0
  123. package/server/src/Models/Order.php +20 -7
  124. package/server/src/Models/Place.php +11 -3
  125. package/server/src/Observers/PurchaseRateObserver.php +2 -1
  126. package/server/src/Providers/FleetOpsServiceProvider.php +26 -0
  127. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +88 -0
  128. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +108 -0
  129. package/server/src/Support/Ai/Capabilities/ConsoleNavigationCapability.php +55 -0
  130. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +508 -0
  131. package/server/src/Support/Ai/Capabilities/DocsHelpCapability.php +49 -0
  132. package/server/src/Support/Ai/Capabilities/ImportOrdersPreviewCapability.php +66 -0
  133. package/server/src/Support/Ai/Capabilities/OperationalQueryCapability.php +319 -0
  134. package/server/src/Support/Ai/Capabilities/OptimizeOrderRouteCapability.php +240 -0
  135. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +95 -0
  136. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +195 -0
  137. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +150 -0
  138. package/server/src/Support/Analytics/RevenueTrend.php +3 -8
  139. package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
  140. package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
  141. package/server/src/Support/Metrics/EarningsMetric.php +1 -10
  142. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +104 -5
  143. package/server/src/Support/Telematics/Providers/SafeeProvider.php +550 -54
  144. package/server/src/Support/Telematics/TelematicService.php +28 -9
  145. package/server/src/Support/Utils.php +52 -0
  146. package/server/src/routes.php +67 -14
  147. package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
  148. package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
  149. package/server/tests/ExportCoverageTest.php +79 -0
  150. package/server/tests/FleetHierarchyResourceTest.php +12 -0
  151. package/server/tests/MetricsRegistryTest.php +45 -5
  152. package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
  153. package/server/tests/PointResolutionTest.php +63 -0
  154. package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
  155. package/server/tests/TelematicsHardeningTest.php +636 -1
  156. package/addon/components/table/cell/dropdown/action-item.hbs +0 -31
  157. package/addon/components/table/cell/dropdown/action-item.js +0 -62
  158. package/addon/components/table/cell/dropdown.hbs +0 -28
  159. package/addon/components/table/cell/dropdown.js +0 -91
  160. package/app/components/table/cell/dropdown/action-item.js +0 -1
  161. package/app/components/table/cell/dropdown.js +0 -1
@@ -1,5 +1,5 @@
1
1
  <ContentPanel @title="Configure Map" @open={{true}} @wrapperClass="bordered-classic">
2
- <InputGroup @name="Default Map Provider" @helpText="Sets the default map provider for all organizations unless they override it in their own FleetOps map settings.">
2
+ <InputGroup @name="Default Map Provider" @helpText="Sets the default map provider for all organizations unless they override it in their own Fleet-Ops map settings.">
3
3
  <Select
4
4
  class="w-full flex-1"
5
5
  @value={{this.mapProvider}}
@@ -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
+ }