@fleetbase/fleetops-engine 0.6.44 → 0.6.46

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 (172) hide show
  1. package/addon/components/admin/map-settings.hbs +26 -0
  2. package/addon/components/admin/map-settings.js +46 -0
  3. package/addon/components/admin/routing-settings.hbs +11 -0
  4. package/addon/components/cell/order-route-type.hbs +46 -0
  5. package/addon/components/cell/order-route-type.js +90 -0
  6. package/addon/components/cell/place-address.hbs +1 -0
  7. package/addon/components/display-place.hbs +45 -56
  8. package/addon/components/display-place.js +21 -0
  9. package/addon/components/entity/form.hbs +24 -0
  10. package/addon/components/entity/form.js +30 -0
  11. package/addon/components/layout/fleet-ops-sidebar/driver-listing.js +5 -5
  12. package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +5 -5
  13. package/addon/components/layout/fleet-ops-sidebar.js +8 -0
  14. package/addon/components/leaflet-draw-control.js +1 -1
  15. package/addon/components/maintenance-schedule/form.js +2 -1
  16. package/addon/components/map/drawer/driver-listing.js +5 -5
  17. package/addon/components/map/drawer/geofence-event-listing.js +5 -2
  18. package/addon/components/map/drawer/place-listing.js +5 -5
  19. package/addon/components/map/drawer/position-listing.js +102 -21
  20. package/addon/components/map/drawer/vehicle-listing.js +5 -5
  21. package/addon/components/map/google-live-map.hbs +9 -0
  22. package/addon/components/map/google-live-map.js +445 -0
  23. package/addon/components/map/leaflet-live-map.hbs +186 -142
  24. package/addon/components/map/leaflet-live-map.js +121 -41
  25. package/addon/components/map/toolbar/visibility-control-panel.js +2 -2
  26. package/addon/components/map/toolbar/zones-panel.hbs +9 -9
  27. package/addon/components/map/toolbar.hbs +3 -3
  28. package/addon/components/map/toolbar.js +9 -1
  29. package/addon/components/modals/map-layer-form.hbs +132 -63
  30. package/addon/components/modals/orchestrator-import.hbs +4 -11
  31. package/addon/components/modals/orchestrator-import.js +17 -1
  32. package/addon/components/modals/update-order-activity.hbs +1 -1
  33. package/addon/components/orchestrator/order-pool.js +3 -2
  34. package/addon/components/orchestrator-workbench.js +29 -41
  35. package/addon/components/order/details/detail.js +2 -2
  36. package/addon/components/order/details/payload.hbs +5 -5
  37. package/addon/components/order/details/payload.js +18 -1
  38. package/addon/components/order/details/route.hbs +2 -2
  39. package/addon/components/order/details/route.js +76 -3
  40. package/addon/components/order/form/payload.hbs +33 -0
  41. package/addon/components/order/form/payload.js +1 -0
  42. package/addon/components/order/form/route.hbs +8 -8
  43. package/addon/components/order/form/route.js +78 -18
  44. package/addon/components/order/form/service-rate.hbs +1 -1
  45. package/addon/components/order/form.js +2 -2
  46. package/addon/components/order/route-editor.hbs +209 -147
  47. package/addon/components/order/route-editor.js +143 -102
  48. package/addon/components/place/address.hbs +1 -0
  49. package/addon/components/place/address.js +12 -0
  50. package/addon/components/route-list.hbs +43 -55
  51. package/addon/components/route-list.js +47 -0
  52. package/addon/components/route-optimization-wizard-panel.js +3 -4
  53. package/addon/components/service-rate/details.hbs +6 -6
  54. package/addon/components/service-rate/form.hbs +28 -15
  55. package/addon/components/vehicle/card.hbs +4 -4
  56. package/addon/controllers/maintenance/schedules/index.js +2 -1
  57. package/addon/controllers/operations/orders/index/details/virtual.js +5 -0
  58. package/addon/controllers/operations/orders/index/details.js +113 -15
  59. package/addon/controllers/operations/orders/index/new.js +1 -1
  60. package/addon/controllers/operations/orders/index.js +8 -1
  61. package/addon/controllers/operations/service-rates/index/edit.js +1 -0
  62. package/addon/controllers/operations/service-rates/index/new.js +1 -0
  63. package/addon/controllers/settings/map.js +97 -0
  64. package/addon/controllers/settings/routing.js +47 -12
  65. package/addon/extension.js +11 -0
  66. package/addon/helpers/f-to-int.js +6 -0
  67. package/addon/helpers/place-address-html.js +6 -0
  68. package/addon/instance-initializers/register-google-routes.js +17 -0
  69. package/addon/instance-initializers/register-osrm.js +5 -0
  70. package/addon/routes/application.js +9 -3
  71. package/addon/routes/operations/orchestrator.js +13 -0
  72. package/addon/routes/operations/orders/index/details/virtual.js +1 -0
  73. package/addon/routes/operations/orders/index/details.js +47 -24
  74. package/addon/routes/operations/orders/index/new.js +9 -0
  75. package/addon/routes/operations/orders/index.js +0 -1
  76. package/addon/routes/settings/map.js +3 -0
  77. package/addon/routes.js +1 -0
  78. package/addon/services/entity-actions.js +1 -1
  79. package/addon/services/equipment-actions.js +4 -6
  80. package/addon/services/geofence-event-bus.js +2 -1
  81. package/addon/services/geofence.js +204 -168
  82. package/addon/services/google-routes.js +300 -0
  83. package/addon/services/leaflet-draw-restriction.js +2 -1
  84. package/addon/services/leaflet-map-manager.js +21 -0
  85. package/addon/services/maintenance-actions.js +4 -6
  86. package/addon/services/map-adapter/google.js +2205 -0
  87. package/addon/services/map-adapter/leaflet.js +1042 -0
  88. package/addon/services/map-adapter-interface.js +551 -0
  89. package/addon/services/map-manager.js +768 -0
  90. package/addon/services/map-settings.js +75 -0
  91. package/addon/services/movement-tracker.js +83 -32
  92. package/addon/services/order-actions.js +11 -1
  93. package/addon/services/osrm.js +79 -6
  94. package/addon/services/part-actions.js +4 -6
  95. package/addon/services/position-playback.js +135 -62
  96. package/addon/services/route-engine.js +83 -0
  97. package/addon/services/service-areas.js +8 -1
  98. package/addon/services/service-rate-actions.js +8 -3
  99. package/addon/styles/fleetops-engine.css +555 -0
  100. package/addon/templates/management/vehicles/index.hbs +0 -1
  101. package/addon/templates/operations/orders/index/details/virtual.hbs +7 -1
  102. package/addon/templates/operations/orders/index/details.hbs +0 -1
  103. package/addon/templates/settings/map.hbs +53 -0
  104. package/addon/templates/settings/routing.hbs +15 -4
  105. package/addon/utils/fleet-ops-options.js +34 -0
  106. package/addon/utils/has-valid-coordinates.js +6 -0
  107. package/addon/utils/map-provider-options.js +6 -0
  108. package/addon/utils/order-route-editing.js +86 -0
  109. package/addon/utils/order-route-summary.js +20 -0
  110. package/addon/utils/place-address-html.js +41 -0
  111. package/addon/utils/route-colors.js +43 -12
  112. package/addon/utils/route-visualization.js +93 -0
  113. package/addon/utils/serialize-payload.js +1 -1
  114. package/addon/utils/vendor-integration.js +2 -1
  115. package/app/components/admin/map-settings.js +1 -0
  116. package/app/components/admin/routing-settings.js +1 -0
  117. package/app/components/cell/order-route-type.js +1 -0
  118. package/app/components/cell/place-address.js +1 -0
  119. package/app/components/map/google-live-map.js +1 -0
  120. package/app/components/place/address.js +1 -0
  121. package/app/controllers/operations/orders/index/details/virtual.js +1 -0
  122. package/app/controllers/settings/map.js +1 -0
  123. package/app/helpers/f-to-int.js +1 -0
  124. package/app/helpers/place-address-html.js +1 -0
  125. package/app/routes/settings/map.js +1 -0
  126. package/app/services/map-adapter/google.js +1 -0
  127. package/app/services/map-adapter/leaflet.js +1 -0
  128. package/app/services/map-adapter-interface.js +1 -0
  129. package/app/services/map-manager.js +1 -0
  130. package/app/services/map-settings.js +1 -0
  131. package/app/templates/settings/map.js +1 -0
  132. package/app/utils/has-valid-coordinates.js +1 -0
  133. package/app/utils/order-route-editing.js +2 -0
  134. package/app/utils/order-route-summary.js +1 -0
  135. package/app/utils/place-address-html.js +1 -0
  136. package/app/utils/route-colors.js +1 -1
  137. package/app/utils/route-visualization.js +1 -0
  138. package/assets/images/drawing-controls-spritesheet.svg +156 -0
  139. package/composer.json +1 -1
  140. package/extension.json +1 -1
  141. package/package.json +3 -3
  142. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  143. package/server/src/Http/Controllers/Api/v1/OrderController.php +36 -8
  144. package/server/src/Http/Controllers/Api/v1/PayloadController.php +14 -1
  145. package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +11 -3
  146. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -0
  147. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +7 -2
  148. package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +6 -1
  149. package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -0
  150. package/server/src/Http/Controllers/Internal/v1/LiveController.php +2 -1
  151. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +7 -4
  152. package/server/src/Http/Controllers/Internal/v1/OrderController.php +61 -34
  153. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +3 -2
  154. package/server/src/Http/Controllers/Internal/v1/SettingController.php +106 -3
  155. package/server/src/Http/Filter/OrderFilter.php +2 -1
  156. package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
  157. package/server/src/Http/Resources/v1/Index/Payload.php +9 -7
  158. package/server/src/Http/Resources/v1/Order.php +20 -2
  159. package/server/src/Http/Resources/v1/ServiceQuote.php +2 -2
  160. package/server/src/Models/Entity.php +7 -0
  161. package/server/src/Models/Order.php +81 -8
  162. package/server/src/Models/OrderConfig.php +15 -0
  163. package/server/src/Models/Payload.php +131 -27
  164. package/server/src/Models/Place.php +182 -48
  165. package/server/src/Models/ServiceRate.php +125 -32
  166. package/server/src/Observers/PurchaseRateObserver.php +3 -5
  167. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +59 -23
  168. package/server/src/Rules/ComputableAlgo.php +1 -3
  169. package/server/src/Support/Algo.php +87 -1
  170. package/server/src/Support/Utils.php +69 -0
  171. package/server/src/routes.php +4 -0
  172. package/translations/en-us.yaml +1937 -1916
@@ -176,7 +176,7 @@
176
176
  </tr>
177
177
  </thead>
178
178
  <tbody>
179
- {{#each @resource.rate_fees as |rateFee|}}
179
+ {{#each @resource.rateFees as |rateFee|}}
180
180
  <tr>
181
181
  <td>
182
182
  <Input
@@ -252,25 +252,38 @@
252
252
  {{else if @resource.isAlgorithm}}
253
253
  <ContentPanel @title={{t "service-rate.fields.custom-algorithm-title"}} @open={{true}} @wrapperClass="bordered-top">
254
254
  <InfoBlock class="mb-5">
255
- {{t "service-rate.fields.custom-algorithm-info-message"}}
256
- {{t "service-rate.fields.custom-algorithm-info-second-message"}}
255
+ Define a custom formula for this service rate using variables wrapped in a single pair of curly braces.
257
256
 
258
257
  <div class="block my-4 break-text">
259
258
  <ul class="list-disc space-y-2 pl-16">
260
- <li class="leading-5"><i>{{t "service-rate.fields.distance-message"}}</i>
261
- {{t "service-rate.fields.distance-continue-message"}}</li>
262
- <li class="leading-5"><i>{{t "service-rate.fields.time-message"}}</i>
263
- {{t "service-rate.fields.time-continue-message"}}</li>
259
+ <li class="leading-5"><code>&#123;distance&#125;</code>, <code>&#123;distance_m&#125;</code>, <code>&#123;distance_km&#125;</code>, <code>&#123;distance_mi&#125;</code>: route distance in meters, kilometers, or miles.</li>
260
+ <li class="leading-5"><code>&#123;time&#125;</code>, <code>&#123;time_s&#125;</code>, <code>&#123;time_min&#125;</code>: route time in seconds or minutes.</li>
261
+ <li class="leading-5"><code>&#123;stops&#125;</code>: total service stops including pickup, dropoff, and intermediate waypoints.</li>
262
+ <li class="leading-5"><code>&#123;waypoints&#125;</code>: intermediate waypoint count only.</li>
263
+ <li class="leading-5"><code>&#123;parcels&#125;</code>, <code>&#123;entities&#125;</code>: parcel count and total payload entity count.</li>
264
+ <li class="leading-5"><code>&#123;base_fee&#125;</code>: the configured base fee for this service rate.</li>
265
+ <li class="leading-5">Supported functions: <code>max(a,b)</code>, <code>min(a,b)</code>, <code>ceil(x)</code>, <code>floor(x)</code>, <code>round(x)</code>.</li>
264
266
  </ul>
265
267
  </div>
266
268
 
267
- <div>
268
- <h4 class="mb-1 text-sm font-semibold">{{t "service-rate.fields.example"}}</h4>
269
- <div class="mb-3 text-inherit">
270
- {{t "service-rate.fields.example-message"}}
271
- {{t "service-rate.fields.example-second-message"}}
269
+ <div class="space-y-3">
270
+ <div>
271
+ <h4 class="mb-1 text-sm font-semibold">{{t "service-rate.fields.example"}} 1</h4>
272
+ <div class="mb-2 text-inherit">Charge by distance in miles after the first 15 miles, plus the base fee.</div>
273
+ <code class="text-xs font-mono">max(&#123;distance_mi&#125; - 15, 0) * 5 + &#123;base_fee&#125;</code>
274
+ </div>
275
+
276
+ <div>
277
+ <h4 class="mb-1 text-sm font-semibold">{{t "service-rate.fields.example"}} 2</h4>
278
+ <div class="mb-2 text-inherit">Charge extra for stops after the first 2 service stops.</div>
279
+ <code class="text-xs font-mono">max(&#123;stops&#125; - 2, 0) * 10 + &#123;base_fee&#125;</code>
280
+ </div>
281
+
282
+ <div>
283
+ <h4 class="mb-1 text-sm font-semibold">{{t "service-rate.fields.example"}} 3</h4>
284
+ <div class="mb-2 text-inherit">Combine waypoint, parcel, and distance surcharges in one formula.</div>
285
+ <code class="text-xs font-mono">max(&#123;waypoints&#125;, 0) * 15 + max(&#123;parcels&#125; - 3, 0) * 2 + max(&#123;distance_km&#125; - 25, 0) * 0.5 + &#123;base_fee&#125;</code>
272
286
  </div>
273
- <code class="text-xs font-mono">(( {distance} / 50 ) * .05 ) + (( {time} / 60 ) * .01)</code>
274
287
  </div>
275
288
  </InfoBlock>
276
289
 
@@ -287,7 +300,7 @@
287
300
  {{else if @resource.isParcelService}}
288
301
  <ContentPanel @title={{t "service-rate.fields.percel-fee-title"}} @open={{true}} @wrapperClass="bordered-top">
289
302
  <div class="space-y-2">
290
- {{#each @resource.parcel_fees as |parcelFee|}}
303
+ {{#each @resource.parcelFees as |parcelFee|}}
291
304
  <div class="dark:text-gray-100">
292
305
  <div class="grid grid-cols-7">
293
306
  <div class="flex flex-col items-start justify-start pt-3">
@@ -541,4 +554,4 @@
541
554
  </InputGroup>
542
555
  {{/if}}
543
556
  </ContentPanel>
544
- </div>
557
+ </div>
@@ -1,7 +1,7 @@
1
1
  <Layout::Resource::Card ...attributes as |Card|>
2
- <Card.header class="{{@headerClass}} truncate">
3
- <div class="font-semibold">{{or @resource.name @resource.yearMakeModel}}</div>
4
- <div class="text-gray-300 dark:text-gray-500 text-sm">{{or @resource.plate_number @resource.vin @resource.serial_number @resource.call_sign @resource.public_id}}</div>
2
+ <Card.header class="{{@headerClass}}">
3
+ <div class="font-semibold w-full truncate">{{or @resource.name @resource.yearMakeModel}}</div>
4
+ <div class="text-gray-300 dark:text-gray-500 text-sm w-full truncate">{{or @resource.plate_number @resource.vin @resource.serial_number @resource.call_sign @resource.public_id}}</div>
5
5
  {{#if (has-block "header")}}
6
6
  {{yield to="header"}}
7
7
  {{/if}}
@@ -21,7 +21,7 @@
21
21
  {{#if (has-block "footer")}}
22
22
  {{yield to="footer"}}
23
23
  {{/if}}
24
- <div class="mt-1">
24
+ <div class="mt-1.5">
25
25
  <div class="text-gray-300 dark:text-gray-500 text-xs">Last Modified: {{@resource.updatedAt}}</div>
26
26
  </div>
27
27
  </Card.footer>
@@ -1,4 +1,5 @@
1
1
  import Controller from '@ember/controller';
2
+ import { isArray } from '@ember/array';
2
3
  import { inject as service } from '@ember/service';
3
4
  import { tracked } from '@glimmer/tracking';
4
5
  import { action } from '@ember/object';
@@ -75,7 +76,7 @@ export default class MaintenanceSchedulesIndexController extends Controller {
75
76
  })
76
77
  .then((response) => {
77
78
  // The API returns { events: [...] } — unwrap the array.
78
- const raw = Array.isArray(response) ? response : (response?.events ?? []);
79
+ const raw = isArray(response) ? response : (response?.events ?? []);
79
80
  const events = raw.map((event) => ({
80
81
  id: event.id,
81
82
  title: event.title,
@@ -0,0 +1,5 @@
1
+ import Controller, { inject as controller } from '@ember/controller';
2
+
3
+ export default class OperationsOrdersIndexDetailsVirtualController extends Controller {
4
+ @controller('operations.orders.index.details') parent;
5
+ }
@@ -1,21 +1,73 @@
1
1
  import Controller, { inject as controller } from '@ember/controller';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
- import { action } from '@ember/object';
5
4
  import { isArray } from '@ember/array';
6
5
  import { task } from 'ember-concurrency';
6
+ import { colorForId, routeColorForStatus, routeStyleForStatus } from '../../../../utils/route-colors';
7
+ import { buildRoutePointMarkerPresentation, buildRoutePointsFromPayload } from '../../../../utils/route-visualization';
7
8
 
8
9
  export default class OperationsOrdersIndexDetailsController extends Controller {
9
10
  @controller('operations.orders.index') index;
10
11
  @service('universe/menu-service') menuService;
11
12
  @service orderActions;
12
13
  @service orderSocketEvents;
13
- @service leafletMapManager;
14
+ @service mapManager;
14
15
  @service leafletLayerVisibilityManager;
15
16
  @service hostRouter;
16
17
  @service universe;
17
18
  @service sidebar;
18
19
  @tracked routingControl;
20
+ @tracked routingCompleted = false;
21
+ @tracked realtimeOrderPublicId = null;
22
+
23
+ get routePoints() {
24
+ return buildRoutePointsFromPayload(this.model?.payload);
25
+ }
26
+
27
+ get routeMarkerFactory() {
28
+ const routeColor = colorForId(this.model.public_id ?? this.model.id ?? 'order-route');
29
+
30
+ return (_waypoint, index) => {
31
+ return buildRoutePointMarkerPresentation(this.routePoints[index], routeColor);
32
+ };
33
+ }
34
+
35
+ get routeMarkerWaypoints() {
36
+ return this.routePoints.map(({ place }) => [place.latitude, place.longitude]);
37
+ }
38
+
39
+ get routeStatus() {
40
+ return this.model.status ?? 'created';
41
+ }
42
+
43
+ get routePolylineOptions() {
44
+ const color = routeColorForStatus(this.routeStatus);
45
+ const styles = routeStyleForStatus(this.routeStatus, color);
46
+
47
+ return {
48
+ color,
49
+ weight: styles.at(-1)?.weight ?? 4,
50
+ opacity: styles.at(-1)?.opacity ?? 0.85,
51
+ styles,
52
+ };
53
+ }
54
+
55
+ get routeControlTag() {
56
+ return this.model?.public_id ? `order-details:${this.model.public_id}` : null;
57
+ }
58
+
59
+ get routingControlOptions() {
60
+ return {
61
+ tag: this.routeControlTag,
62
+ color: this.routePolylineOptions.color,
63
+ status: this.routeStatus,
64
+ markerWaypoints: this.routeMarkerWaypoints,
65
+ polylineOptions: this.routePolylineOptions,
66
+ createMarker: this.routeMarkerFactory,
67
+ onRouteFound: this.handleRoutingComplete,
68
+ onRoutingError: this.handleRoutingComplete,
69
+ };
70
+ }
19
71
 
20
72
  get tabs() {
21
73
  const registeredTabs = this.menuService.getMenuItems('fleet-ops:component:order:details');
@@ -103,32 +155,78 @@ export default class OperationsOrdersIndexDetailsController extends Controller {
103
155
  }
104
156
 
105
157
  @task *refresh() {
106
- yield this.hostRouter.refresh();
158
+ try {
159
+ yield this.hostRouter.refresh();
160
+ } catch (error) {
161
+ return error;
162
+ }
107
163
  }
108
164
 
109
- @action async setup() {
110
- // Change to map layout and display order route
111
- this.index.changeLayout('map');
112
- this.routingControl = await this.leafletMapManager.addRoutingControl(this.model.routeWaypoints);
165
+ handleRoutingComplete = () => {
166
+ this.routingCompleted = true;
167
+ };
113
168
 
114
- // Hide sidebar
115
- this.sidebar.hideNow();
169
+ async setupRealtime() {
170
+ const currentOrderPublicId = this.model?.public_id;
171
+ if (!currentOrderPublicId) {
172
+ return;
173
+ }
116
174
 
117
- // Show & track driver assigned
118
- this.leafletLayerVisibilityManager.hideCategory('drivers');
119
- this.leafletLayerVisibilityManager.showModelLayer(this.model.driver_assigned);
120
- // Should we hide places & other vehicles?
175
+ if (this.realtimeOrderPublicId && this.realtimeOrderPublicId !== currentOrderPublicId) {
176
+ this.orderSocketEvents.stop({ public_id: this.realtimeOrderPublicId });
177
+ }
178
+
179
+ this.realtimeOrderPublicId = currentOrderPublicId;
121
180
 
122
- // Listen for this order events
123
181
  this.orderSocketEvents.start(
124
182
  this.model,
125
183
  async (_msg, { reloadable }) => {
126
184
  if (reloadable) {
127
185
  await this.hostRouter.refresh();
128
- this.leafletMapManager.replaceRoutingControl(this.model.routeWaypoints, this.routingControl);
129
186
  }
130
187
  },
131
188
  { debounceMs: 250 }
132
189
  );
133
190
  }
191
+
192
+ async syncRoutingControl() {
193
+ this.routingCompleted = false;
194
+ this.teardownRoutingControls();
195
+
196
+ this.routingControl = await this.mapManager.addRoutingControl(this.model.routeWaypoints, this.routingControlOptions);
197
+
198
+ if (!this.routingControl) {
199
+ this.routingCompleted = true;
200
+ }
201
+ }
202
+
203
+ syncMapContext() {
204
+ this.leafletLayerVisibilityManager.hideCategory('drivers');
205
+ this.leafletLayerVisibilityManager.showModelLayer(this.model.driver_assigned);
206
+ }
207
+
208
+ teardownRoutingControls() {
209
+ if (this.routeControlTag) {
210
+ this.mapManager.clearRoutingControlsByTag(this.routeControlTag);
211
+ } else if (this.routingControl) {
212
+ this.mapManager.removeRoutingControl(this.routingControl);
213
+ }
214
+
215
+ this.routingControl = undefined;
216
+ this.routingCompleted = false;
217
+ }
218
+
219
+ teardownRealtime() {
220
+ if (this.realtimeOrderPublicId) {
221
+ this.orderSocketEvents.stop({ public_id: this.realtimeOrderPublicId });
222
+ this.realtimeOrderPublicId = null;
223
+ }
224
+ }
225
+
226
+ async setupDetailsSession() {
227
+ this.index.changeLayout('map');
228
+ await this.setupRealtime();
229
+ await this.syncRoutingControl();
230
+ this.syncMapContext();
231
+ }
134
232
  }
@@ -20,6 +20,7 @@ export default class OperationsOrdersIndexNewController extends Controller {
20
20
  @service orderCreation;
21
21
  @service orderValidation;
22
22
  @service events;
23
+ @service sidebar;
23
24
  @tracked order = this.orderCreation.newOrder();
24
25
  @tracked overlay;
25
26
 
@@ -92,7 +93,6 @@ export default class OperationsOrdersIndexNewController extends Controller {
92
93
  }
93
94
 
94
95
  @action setup() {
95
- // Change to map layout
96
96
  this.index.changeLayout('map');
97
97
  }
98
98
 
@@ -142,6 +142,13 @@ export default class OperationsOrdersIndexController extends Controller {
142
142
  filterable: true,
143
143
  filterComponent: 'filter/string',
144
144
  },
145
+ {
146
+ label: this.intl.t('column.route-type'),
147
+ valuePath: 'payload.waypoints.length',
148
+ cellComponent: 'cell/order-route-type',
149
+ cellClassNames: 'overflow-visible',
150
+ resizable: true,
151
+ },
145
152
  {
146
153
  label: this.intl.t('column.internal-id'),
147
154
  valuePath: 'internal_id',
@@ -283,7 +290,7 @@ export default class OperationsOrdersIndexController extends Controller {
283
290
  label: this.intl.t('column.type'),
284
291
  valuePath: 'type',
285
292
  resizable: true,
286
- hidden: true,
293
+ humanize: true,
287
294
  sortable: true,
288
295
  filterable: true,
289
296
  filterComponent: 'filter/model',
@@ -22,6 +22,7 @@ export default class OperationsServiceRatesIndexEditController extends Controlle
22
22
  @task *save(serviceRate) {
23
23
  try {
24
24
  yield serviceRate.save();
25
+ yield serviceRate.reload();
25
26
  this.events.trackResourceUpdated(serviceRate);
26
27
  this.overlay?.close();
27
28
 
@@ -16,6 +16,7 @@ export default class OperationsServiceRatesIndexNewController extends Controller
16
16
  @task *save(serviceRate) {
17
17
  try {
18
18
  yield serviceRate.save();
19
+ yield serviceRate.reload();
19
20
  this.events.trackResourceCreated(serviceRate);
20
21
  this.overlay?.close();
21
22
 
@@ -0,0 +1,97 @@
1
+ import Controller from '@ember/controller';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { action } from '@ember/object';
5
+ import { task } from 'ember-concurrency';
6
+ import MAP_PROVIDER_OPTIONS from '../../utils/map-provider-options';
7
+
8
+ export default class SettingsMapController extends Controller {
9
+ @service fetch;
10
+ @service notifications;
11
+ @service intl;
12
+ @service mapManager;
13
+ @service mapSettings;
14
+
15
+ /**
16
+ * Available map provider options for the provider selector.
17
+ *
18
+ * @memberof SettingsMapController
19
+ * @var {Array}
20
+ */
21
+ @tracked mapProviderOptions = MAP_PROVIDER_OPTIONS;
22
+
23
+ /**
24
+ * The currently-selected map provider key (e.g. 'leaflet' or 'google').
25
+ *
26
+ * @memberof SettingsMapController
27
+ * @var {String}
28
+ */
29
+ @tracked mapProvider = 'leaflet';
30
+
31
+ /**
32
+ * Whether settings have been loaded from the server.
33
+ *
34
+ * @memberof SettingsMapController
35
+ * @var {Boolean}
36
+ */
37
+ @tracked settingsLoaded = false;
38
+
39
+ constructor() {
40
+ super(...arguments);
41
+ this.getSettings.perform();
42
+ }
43
+
44
+ // ─── Computed getters ──────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * True when the user has selected Google Maps as the provider.
48
+ *
49
+ * @memberof SettingsMapController
50
+ * @return {Boolean}
51
+ */
52
+ get isGoogleMapsSelected() {
53
+ return this.mapProvider === 'google';
54
+ }
55
+
56
+ // ─── Actions ───────────────────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Called when the user picks a different map provider from the Select.
60
+ *
61
+ * @param {Object|String} option The selected option object or raw value.
62
+ * @memberof SettingsMapController
63
+ */
64
+ @action selectMapProvider(option) {
65
+ this.mapProvider = option?.value ?? option ?? 'leaflet';
66
+ }
67
+
68
+ @task *saveSettings() {
69
+ const settings = {
70
+ mapProvider: this.mapProvider,
71
+ };
72
+
73
+ try {
74
+ const response = yield this.fetch.post('fleet-ops/settings/map', { settings });
75
+ this.mapSettings.applySettings(response);
76
+
77
+ if (this.mapManager && typeof this.mapManager.setActiveProvider === 'function') {
78
+ this.mapManager.setActiveProvider(this.mapProvider);
79
+ }
80
+
81
+ this.notifications.success(this.intl.t('settings.map.settings-saved'));
82
+ } catch (error) {
83
+ this.notifications.serverError(error);
84
+ }
85
+ }
86
+
87
+ @task *getSettings() {
88
+ try {
89
+ const response = yield this.mapSettings.load({ force: true });
90
+
91
+ this.mapProvider = response?.mapProvider ?? 'leaflet';
92
+ this.settingsLoaded = true;
93
+ } catch (error) {
94
+ this.notifications.serverError(error);
95
+ }
96
+ }
97
+ }
@@ -7,14 +7,20 @@ export default class SettingsRoutingController extends Controller {
7
7
  @service fetch;
8
8
  @service notifications;
9
9
  @service currentUser;
10
- @service leafletRoutingControl;
11
- @tracked routerService = 'osrm';
10
+ @service routeEngine;
11
+ @service routeOptimization;
12
+ @tracked displayEngine = 'osrm';
13
+ @tracked optimizationEngine = 'osrm';
12
14
  @tracked routingUnit = 'km';
13
15
  @tracked routingUnitOptions = [
14
16
  { label: 'Kilometers', value: 'km' },
15
17
  { label: 'Miles', value: 'mi' },
16
18
  ];
17
- @tracked saveTasks = [];
19
+ saveTasks = new Map();
20
+
21
+ get hasEngineOverrides() {
22
+ return ['vroom', 'valhalla'].includes(this.displayEngine) || ['vroom', 'valhalla'].includes(this.optimizationEngine);
23
+ }
18
24
 
19
25
  constructor() {
20
26
  super(...arguments);
@@ -28,10 +34,19 @@ export default class SettingsRoutingController extends Controller {
28
34
  */
29
35
  @task *saveSettings() {
30
36
  try {
31
- yield this.fetch.post('fleet-ops/settings/routing-settings', { router: this.routerService, unit: this.routingUnit });
37
+ yield this.fetch.post('fleet-ops/settings/routing-settings', {
38
+ display_engine: this.displayEngine,
39
+ optimization_engine: this.optimizationEngine,
40
+ unit: this.routingUnit,
41
+ });
32
42
  yield this.performAdditionalSaveTasks();
33
43
  // Save in local memory too
34
- this.currentUser.setOption('routing', { router: this.routerService, unit: this.routingUnit });
44
+ this.currentUser.setOption('routing', {
45
+ router: this.displayEngine,
46
+ routing_display_engine: this.displayEngine,
47
+ routing_optimization_engine: this.optimizationEngine,
48
+ unit: this.routingUnit,
49
+ });
35
50
  this.notifications.success('Routing setting saved.');
36
51
  } catch (error) {
37
52
  this.notifications.serverError(error);
@@ -45,23 +60,43 @@ export default class SettingsRoutingController extends Controller {
45
60
  */
46
61
  @task *getSettings() {
47
62
  try {
48
- const { router, unit } = yield this.fetch.get('fleet-ops/settings/routing-settings');
49
- this.routerService = router;
63
+ const { router, display_engine, optimization_engine, unit } = yield this.fetch.get('fleet-ops/settings/routing-settings');
64
+ this.displayEngine = display_engine ?? router ?? 'osrm';
65
+ this.optimizationEngine = optimization_engine ?? display_engine ?? router ?? 'osrm';
50
66
  this.routingUnit = unit;
51
67
  } catch (error) {
52
68
  this.notifications.serverError(error);
53
69
  }
54
70
  }
55
71
 
56
- registerSaveTask(task) {
57
- this.saveTasks.push(task);
72
+ registerSaveTask(key, task) {
73
+ if (!key || typeof task?.perform !== 'function') {
74
+ return;
75
+ }
76
+
77
+ this.saveTasks.set(key, task);
78
+ }
79
+
80
+ unregisterSaveTask(key) {
81
+ if (!key) {
82
+ return;
83
+ }
84
+
85
+ this.saveTasks.delete(key);
58
86
  }
59
87
 
60
88
  async performAdditionalSaveTasks() {
61
- for (let i = 0; i < this.saveTasks.length; i++) {
62
- const task = this.saveTasks[i];
63
- if (typeof task.perform === 'function') {
89
+ for (const task of this.saveTasks.values()) {
90
+ try {
64
91
  await task.perform();
92
+ } catch (error) {
93
+ // Ignore explicit ember-concurrency task cancellations from components
94
+ // that may have unmounted during routing-engine selection changes.
95
+ if (typeof error?.name === 'string' && error.name.includes('TaskCancelation')) {
96
+ continue;
97
+ }
98
+
99
+ throw error;
65
100
  }
66
101
  }
67
102
  return true;
@@ -73,6 +73,16 @@ export default {
73
73
  menuService.registerAdminMenuPanel(
74
74
  'Fleet-Ops Config',
75
75
  [
76
+ new MenuItem({
77
+ title: 'Routing',
78
+ icon: 'route',
79
+ component: new ExtensionComponent('@fleetbase/fleetops-engine', 'admin/routing-settings'),
80
+ }),
81
+ new MenuItem({
82
+ title: 'Map',
83
+ icon: 'map',
84
+ component: new ExtensionComponent('@fleetbase/fleetops-engine', 'admin/map-settings'),
85
+ }),
76
86
  new MenuItem({
77
87
  title: 'Navigator App',
78
88
  icon: 'location-arrow',
@@ -179,6 +189,7 @@ export default {
179
189
  'fleet-ops:component:order:form:payload:entity:form',
180
190
  'fleet-ops:template:settings:routing',
181
191
  'fleet-ops:template:settings:orchestrator',
192
+ 'fleet-ops:component:admin:routing-settings',
182
193
  ]);
183
194
  },
184
195
  };
@@ -0,0 +1,6 @@
1
+ import { helper } from '@ember/component/helper';
2
+ import numbersOnly from '@fleetbase/ember-core/utils/numbers-only';
3
+
4
+ export default helper(function fToInt([value]) {
5
+ return parseInt(numbersOnly(value));
6
+ });
@@ -0,0 +1,6 @@
1
+ import { helper } from '@ember/component/helper';
2
+ import placeAddressHtml from '../utils/place-address-html';
3
+
4
+ export default helper(function placeAddressHtmlHelper([place]) {
5
+ return placeAddressHtml(place);
6
+ });
@@ -0,0 +1,17 @@
1
+ export function initialize(owner) {
2
+ const routeOptimization = owner.lookup('service:route-optimization');
3
+ const routeEngine = owner.lookup('service:route-engine');
4
+ const googleRoutes = owner.lookup('service:google-routes');
5
+
6
+ if (routeOptimization && googleRoutes) {
7
+ routeOptimization.register('google', googleRoutes);
8
+ }
9
+
10
+ if (routeEngine && googleRoutes) {
11
+ routeEngine.register('google', googleRoutes, { display: true });
12
+ }
13
+ }
14
+
15
+ export default {
16
+ initialize,
17
+ };
@@ -5,11 +5,16 @@ import { OSRMv1 } from '@fleetbase/leaflet-routing-machine';
5
5
  export function initialize(owner) {
6
6
  // Register OSRM as route optimization service
7
7
  const routeOptimization = owner.lookup('service:route-optimization');
8
+ const routeEngine = owner.lookup('service:route-engine');
8
9
  const osrm = owner.lookup('service:osrm');
9
10
  if (routeOptimization && osrm) {
10
11
  routeOptimization.register('osrm', osrm);
11
12
  }
12
13
 
14
+ if (routeEngine && osrm) {
15
+ routeEngine.register('osrm', osrm, { display: true });
16
+ }
17
+
13
18
  // Register OSRM as Routing Controler
14
19
  const leafletRoutingControl = owner.lookup('service:leaflet-routing-control');
15
20
  if (leafletRoutingControl) {
@@ -12,6 +12,7 @@ export default class ApplicationRoute extends Route {
12
12
  @service notifications;
13
13
  @service fetch;
14
14
  @service currentUser;
15
+ @service mapSettings;
15
16
 
16
17
  @action loading(transition) {
17
18
  const resource = getResourceNameFromTransition(transition, { humanize: true });
@@ -20,18 +21,23 @@ export default class ApplicationRoute extends Route {
20
21
  });
21
22
  }
22
23
 
23
- beforeModel() {
24
+ async beforeModel() {
24
25
  if (this.abilities.cannot('fleet-ops see extension')) {
25
26
  this.notifications.warning(this.intl.t('common.unauthorized-access'));
26
27
  return this.hostRouter.transitionTo('console');
27
28
  }
28
29
 
29
- this.location.getUserLocation();
30
- this.#loadRoutingSettings();
30
+ await this.location.getUserLocation();
31
+ await this.#loadRoutingSettings();
32
+ await this.#loadMapSettings();
31
33
  }
32
34
 
33
35
  async #loadRoutingSettings() {
34
36
  const routingSetting = await this.fetch.get('fleet-ops/settings/routing-settings');
35
37
  this.currentUser.setOption('routing', routingSetting);
36
38
  }
39
+
40
+ async #loadMapSettings() {
41
+ await this.mapSettings.load();
42
+ }
37
43
  }