@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
@@ -228,17 +228,10 @@
228
228
 
229
229
  {{! Order type badge }}
230
230
  <td>
231
- {{#if (eq group.order_type "multi_waypoint")}}
232
- <span class="import-preview-badge import-preview-badge--blue">
233
- <FaIcon @icon="route" class="mr-0.5" />
234
- {{t "orchestrator.col-preview-multi-stop" count=group.waypointCount}}
235
- </span>
236
- {{else}}
237
- <span class="import-preview-badge import-preview-badge--gray">
238
- <FaIcon @icon="exchange-alt" class="mr-0.5" />
239
- {{t "orchestrator.col-preview-pickup-dropoff"}}
240
- </span>
241
- {{/if}}
231
+ <span class="{{group.routeSummary.badgeClass}}">
232
+ <FaIcon @icon={{group.routeSummary.icon}} class="mr-0.5" />
233
+ {{group.routeSummary.label}}
234
+ </span>
242
235
  </td>
243
236
 
244
237
  {{! Pickup }}
@@ -4,6 +4,7 @@ import { tracked } from '@glimmer/tracking';
4
4
  import { action } from '@ember/object';
5
5
  import { task } from 'ember-concurrency';
6
6
  import * as XLSX from 'xlsx';
7
+ import { buildRouteTypeSummary } from '../../utils/order-route-summary';
7
8
 
8
9
  /**
9
10
  * Orchestrator Import Modal
@@ -328,7 +329,22 @@ export default class OrchestratorImportComponent extends Component {
328
329
  if (!g.scheduled_at && row.scheduled_at) g.scheduled_at = row.scheduled_at;
329
330
  });
330
331
 
331
- return Array.from(groups.values());
332
+ return Array.from(groups.values()).map((group) => ({
333
+ ...group,
334
+ routeSummary: this.routeTypeSummaryForGroup(group),
335
+ }));
336
+ }
337
+
338
+ routeTypeSummaryForGroup(group) {
339
+ const baseSummary = buildRouteTypeSummary({
340
+ hasIntermediateWaypoints: group.order_type === 'multi_waypoint' || group.waypointCount > 0,
341
+ intermediateStopCount: group.waypointCount,
342
+ });
343
+
344
+ return {
345
+ ...baseSummary,
346
+ label: this.intl.t(baseSummary.translationKey, baseSummary.translationOptions),
347
+ };
332
348
  }
333
349
 
334
350
  // ── Footer action buttons (injected into modal footer via modalsManager) ──
@@ -65,7 +65,7 @@
65
65
  </div>
66
66
  <label for="activity_custom" class="ml-3 flex flex-col cursor-pointer w-full pr-5">
67
67
  <div class="font-semibold text-sm mb-1">Add custom activity</div>
68
- <span class="block text-sm font-medium">
68
+ <span class="block text-sm font-medium mb-0.5">
69
69
  <Input
70
70
  @type="text"
71
71
  @value={{this.customActivity.status}}
@@ -1,5 +1,6 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
+ import { isArray } from '@ember/array';
3
4
  import { action } from '@ember/object';
4
5
  import { inject as service } from '@ember/service';
5
6
 
@@ -157,7 +158,7 @@ export default class OrchestratorOrderPoolComponent extends Component {
157
158
  stops.push(payload.pickup.address ?? payload.pickup.street1 ?? payload.pickup.name ?? '—');
158
159
  }
159
160
 
160
- const waypoints = typeof payload.waypoints?.toArray === 'function' ? payload.waypoints.toArray() : Array.isArray(payload.waypoints) ? payload.waypoints : [];
161
+ const waypoints = typeof payload.waypoints?.toArray === 'function' ? payload.waypoints.toArray() : isArray(payload.waypoints) ? payload.waypoints : [];
161
162
 
162
163
  for (const wp of waypoints) {
163
164
  const place = wp.place ?? wp;
@@ -318,7 +319,7 @@ export default class OrchestratorOrderPoolComponent extends Component {
318
319
  let value = null;
319
320
 
320
321
  const cfvCollection = order.custom_field_values ?? order.customFieldValues ?? [];
321
- const cfvArray = typeof cfvCollection?.toArray === 'function' ? cfvCollection.toArray() : Array.isArray(cfvCollection) ? cfvCollection : [];
322
+ const cfvArray = typeof cfvCollection?.toArray === 'function' ? cfvCollection.toArray() : isArray(cfvCollection) ? cfvCollection : [];
322
323
 
323
324
  for (const cfv of cfvArray) {
324
325
  const cf = cfv.custom_field ?? (typeof cfv.customField?.get === 'function' ? cfv.customField : null) ?? cfv.customField ?? null;
@@ -1,4 +1,5 @@
1
1
  import Component from '@glimmer/component';
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';
@@ -6,8 +7,6 @@ import { later } from '@ember/runloop';
6
7
  import { debug } from '@ember/debug';
7
8
  import { task } from 'ember-concurrency';
8
9
  import { colorForId, waypointIconHtml } from '../utils/route-colors';
9
- import polyline from '@fleetbase/ember-core/utils/polyline';
10
- import getRoutingHost from '@fleetbase/ember-core/utils/get-routing-host';
11
10
 
12
11
  /**
13
12
  * OrchestratorWorkbenchComponent
@@ -36,7 +35,8 @@ export default class OrchestratorWorkbenchComponent extends Component {
36
35
  @service intl;
37
36
  @service modalsManager;
38
37
  @service location;
39
- @service leafletMapManager;
38
+ @service mapManager;
39
+ @service routeEngine;
40
40
  @service('order-allocation') allocationService;
41
41
 
42
42
  /** Routing controls added to the map for the current proposed plan. */
@@ -152,6 +152,10 @@ export default class OrchestratorWorkbenchComponent extends Component {
152
152
  this.loadCardFields.perform();
153
153
  }
154
154
 
155
+ willDestroy() {
156
+ super.willDestroy(...arguments);
157
+ }
158
+
155
159
  // ── Data loading ──────────────────────────────────────────────────────────
156
160
 
157
161
  @task *loadData() {
@@ -536,10 +540,8 @@ export default class OrchestratorWorkbenchComponent extends Component {
536
540
 
537
541
  @action onMapLoad({ target: map }) {
538
542
  this.leafletMap = map;
539
- // Register the map with the service so addRoutingControl / ensureInteractive
540
- // can resolve it. Without this call, waitForMap() never resolves and
541
- // routing controls silently time out.
542
- this.leafletMapManager.setMap(map);
543
+ this.mapManager.setActiveProvider('leaflet');
544
+ this.mapManager.setMapInstance(map);
543
545
  // If orders have already loaded before the map mounted, re-center now
544
546
  // using the same fitBounds strategy as _centerMapOnOrders().
545
547
  if (this._mapCenteredOnOrders) {
@@ -563,37 +565,28 @@ export default class OrchestratorWorkbenchComponent extends Component {
563
565
  */
564
566
  async _drawRoutingControls() {
565
567
  this._clearRoutingControls();
566
- const map = this.leafletMap;
567
- if (!map) return;
568
568
  const groups = this.planByVehicle;
569
569
  if (!groups.length) return;
570
-
571
- const routingHost = getRoutingHost();
572
570
  const allLatLngs = [];
573
571
 
574
572
  for (const group of groups) {
575
573
  const waypoints = group.routeWaypoints;
576
574
  if (!waypoints || waypoints.length < 2) continue;
577
575
  try {
578
- // OSRM route/v1 expects coordinates as lon,lat pairs joined by semicolons.
579
- const coordStr = waypoints.map(([lat, lng]) => `${lng},${lat}`).join(';');
580
- const url = `${routingHost}/route/v1/driving/${coordStr}?overview=full&geometries=polyline`;
581
- const resp = await fetch(url);
582
- if (!resp.ok) continue;
583
- const data = await resp.json();
584
- const geometry = data?.routes?.[0]?.geometry;
585
- if (!geometry) continue;
586
- // polyline.decode returns [[lat, lng], ...] — exactly what L.polyline expects.
587
- const latlngs = polyline.decode(geometry);
588
- if (!latlngs.length) continue;
589
- const pl = L.polyline(latlngs, {
590
- color: group.routeColor,
591
- weight: 4,
592
- opacity: 0.85,
576
+ const routeHandle = await this.mapManager.addRoutingControl(waypoints, {
577
+ engine: this.routeEngine.getDisplayEngine('osrm'),
578
+ suppressMarkers: true,
579
+ position: false,
580
+ tag: `orchestrator:${group.vehicle?.public_id ?? group.vehicleId ?? group.vehicle_uuid ?? 'unknown'}`,
581
+ polylineOptions: {
582
+ color: group.routeColor,
583
+ weight: 4,
584
+ opacity: 0.85,
585
+ },
593
586
  });
594
- pl.addTo(map);
595
- this._routingControls.push(pl);
596
- allLatLngs.push(...latlngs);
587
+ if (!routeHandle?.route?.coordinates?.length) continue;
588
+ this._routingControls.push(routeHandle);
589
+ allLatLngs.push(...routeHandle.route.coordinates);
597
590
  } catch {
598
591
  // Non-fatal: routing may fail for individual vehicles if OSRM
599
592
  // cannot find a route (e.g. ferry-only legs, missing road data)
@@ -603,7 +596,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
603
596
  // Fit the map to show all drawn routes.
604
597
  if (allLatLngs.length >= 2) {
605
598
  try {
606
- map.fitBounds(allLatLngs, { padding: [40, 40], maxZoom: 14 });
599
+ this.mapManager.fitBounds(allLatLngs, { padding: [40, 40], maxZoom: 14 });
607
600
  } catch {
608
601
  // ignore fitBounds errors
609
602
  }
@@ -614,14 +607,9 @@ export default class OrchestratorWorkbenchComponent extends Component {
614
607
  * Remove all route polylines that were added for the current plan.
615
608
  */
616
609
  _clearRoutingControls() {
617
- const map = this.leafletMap;
618
- for (const layer of this._routingControls) {
610
+ for (const routeHandle of this._routingControls) {
619
611
  try {
620
- if (map) {
621
- map.removeLayer(layer);
622
- } else {
623
- layer.remove?.();
624
- }
612
+ this.mapManager.removeRoutingControl(routeHandle);
625
613
  } catch {
626
614
  // ignore removal errors
627
615
  }
@@ -686,7 +674,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
686
674
  }
687
675
 
688
676
  get hasProposedPlan() {
689
- return Array.isArray(this.proposedPlan) && this.proposedPlan.length > 0;
677
+ return isArray(this.proposedPlan) && this.proposedPlan.length > 0;
690
678
  }
691
679
 
692
680
  get hasUnassigned() {
@@ -792,7 +780,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
792
780
  if (loc) {
793
781
  // GeoJSON Point — with or without the type field.
794
782
  // LaravelMysqlSpatial SpatialExpression may serialize without type.
795
- if (Array.isArray(loc.coordinates) && loc.coordinates.length >= 2) {
783
+ if (isArray(loc.coordinates) && loc.coordinates.length >= 2) {
796
784
  const lng = parseFloat(loc.coordinates[0]);
797
785
  const lat = parseFloat(loc.coordinates[1]);
798
786
  // Use isFinite to accept 0 as a valid coordinate (not just truthy)
@@ -809,7 +797,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
809
797
  }
810
798
  }
811
799
  // Numeric array [lat, lng] (some internal formats)
812
- if (Array.isArray(loc) && loc.length >= 2) {
800
+ if (isArray(loc) && loc.length >= 2) {
813
801
  const lat = parseFloat(loc[0]);
814
802
  const lng = parseFloat(loc[1]);
815
803
  if (isFinite(lat) && isFinite(lng) && (lat !== 0 || lng !== 0)) {
@@ -848,7 +836,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
848
836
  // NOTE: isMultiDrop is an Ember Data computed property and won't exist on
849
837
  // plain JSON objects returned by the orchestrator/orders endpoint.
850
838
  const waypoints = payload.waypoints;
851
- const hasWaypoints = Array.isArray(waypoints) && waypoints.length > 0;
839
+ const hasWaypoints = isArray(waypoints) && waypoints.length > 0;
852
840
  const isMultiDrop = payload.isMultiDrop === true || (hasWaypoints && !payload.pickup && !payload.dropoff);
853
841
  if (isMultiDrop && hasWaypoints) {
854
842
  const sorted = [...waypoints].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
@@ -5,7 +5,7 @@ import { action } from '@ember/object';
5
5
  export default class OrderDetailsDetailComponent extends Component {
6
6
  @service orderActions;
7
7
  @service driverActions;
8
- @service leafletMapManager;
8
+ @service mapManager;
9
9
  @service intl;
10
10
 
11
11
  get actionButtons() {
@@ -37,6 +37,6 @@ export default class OrderDetailsDetailComponent extends Component {
37
37
 
38
38
  @action focusOrderAssignedDriver(driver) {
39
39
  this.driverActions.panel.view(driver);
40
- this.leafletMapManager.map?.flyTo(driver.coordinates, 18);
40
+ this.mapManager.focusResource(driver, 18);
41
41
  }
42
42
  }
@@ -1,5 +1,5 @@
1
1
  <ContentPanel @title={{t "order.fields.payload"}} @open={{true}} @actionButtons={{this.actionButtons}} @wrapperClass="bordered-top">
2
- {{#if @resource.isMultiDrop}}
2
+ {{#if @resource.hasIntermediateWaypoints}}
3
3
  <div class="space-y-2">
4
4
  {{#each @resource.entitiesByDestination as |group|}}
5
5
  <div class="rounded-md border border-gray-200 dark:border-gray-900 p-3">
@@ -25,11 +25,11 @@
25
25
  </div>
26
26
  </div>
27
27
  {{/if}}
28
- <DisplayPlace @place={{group.waypoint}} @addressClass="text-sm" />
29
- <div class="my-2">
30
- <span class="rounded-md bg-yellow-300 text-yellow-900 px-2 py-1 text-sm">{{t "order.fields.tracking"}}
28
+ <DisplayPlace @place={{group.waypoint}} @addressClass="text-sm" @hideBadges={{true}} />
29
+ <div class="flex align-self mt-1">
30
+ <ClickToCopy @value={{group.waypoint.tracking}} class="rounded-md bg-yellow-300 text-yellow-900 px-2 py-1 text-sm">{{t "order.fields.tracking"}}
31
31
  {{group.waypoint.tracking}}
32
- </span>
32
+ </ClickToCopy>
33
33
  </div>
34
34
  </div>
35
35
  <div>
@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
3
3
  import { action } from '@ember/object';
4
4
  import { debug } from '@ember/debug';
5
5
  import { task } from 'ember-concurrency';
6
+ import placeAddressHtml from '../../../utils/place-address-html';
6
7
 
7
8
  export default class OrderDetailsPayloadComponent extends Component {
8
9
  @service entityActions;
@@ -13,7 +14,23 @@ export default class OrderDetailsPayloadComponent extends Component {
13
14
  @service intl;
14
15
 
15
16
  get actionButtons() {
16
- if (this.args.resource.isMultiDrop) return [];
17
+ if (this.args.resource.hasIntermediateWaypoints) {
18
+ const places = this.args.resource.places ?? [];
19
+ return [
20
+ {
21
+ type: 'default',
22
+ text: this.intl.t('order.actions.add-entity'),
23
+ icon: 'plus',
24
+ iconPrefix: 'fas',
25
+ permission: 'fleet-ops update order',
26
+ disabled: this.args.resource.status === 'canceled',
27
+ items: places.map((p) => ({
28
+ text: placeAddressHtml(p),
29
+ onClick: () => this.addEntity.perform(p),
30
+ })),
31
+ },
32
+ ];
33
+ }
17
34
 
18
35
  return [
19
36
  {
@@ -1,5 +1,5 @@
1
1
  <ContentPanel @title={{t "order.fields.route-panel-title"}} @open={{true}} @actionButtons={{this.actionButtons}} @wrapperClass="bordered-top">
2
- {{#if @resource.payload.isMultiDrop}}
2
+ {{#if @resource.hasIntermediateWaypoints}}
3
3
  <div>
4
4
  <RouteList @order={{@resource}} @eta={{@resource.eta}} @waypointActions={{hash viewWaypointLabel=(hash label=(t "order.fields.get-label") fn=this.viewWaypointLabel)}} />
5
5
  </div>
@@ -19,4 +19,4 @@
19
19
  </div>
20
20
  </div>
21
21
  {{/if}}
22
- </ContentPanel>
22
+ </ContentPanel>
@@ -2,14 +2,38 @@ import Component from '@glimmer/component';
2
2
  import { inject as service } from '@ember/service';
3
3
  import { action } from '@ember/object';
4
4
  import { debug } from '@ember/debug';
5
+ import { task } from 'ember-concurrency';
6
+ import { applyOptimizedIntermediateWaypoints, buildRouteOptimizationInput, canOptimizeIntermediateWaypoints } from '../../../utils/order-route-editing';
5
7
 
6
8
  export default class OrderDetailsRouteComponent extends Component {
7
9
  @service orderActions;
8
10
  @service modalsManager;
9
11
  @service fetch;
12
+ @service notifications;
13
+ @service routeEngine;
14
+ @service routeOptimization;
15
+ @service hostRouter;
16
+ @service intl;
10
17
 
11
18
  get actionButtons() {
12
19
  return [
20
+ this.showOptimizationSelect
21
+ ? {
22
+ component: 'route-optimization-engine-select-button',
23
+ onClick: (service) => this.optimizeRouteWithService.perform(service),
24
+ isLoading: this.optimizeRouteWithService.isRunning,
25
+ disabled: !this.canOptimizeRoute,
26
+ }
27
+ : {
28
+ type: 'magic',
29
+ icon: 'magic',
30
+ text: this.intl.t('order.fields.optimize-route'),
31
+ onClick: () => this.optimizeRoute.perform(),
32
+ permission: 'fleet-ops optimize order',
33
+ helpText: this.intl.t('order.fields.optimize-route-help-text'),
34
+ disabled: !this.canOptimizeRoute,
35
+ isLoading: this.optimizeRoute.isRunning,
36
+ },
13
37
  {
14
38
  type: 'default',
15
39
  text: 'Edit',
@@ -24,8 +48,59 @@ export default class OrderDetailsRouteComponent extends Component {
24
48
  ];
25
49
  }
26
50
 
51
+ get canOptimizeRoute() {
52
+ return canOptimizeIntermediateWaypoints(this.args.resource?.payload);
53
+ }
54
+
55
+ get showOptimizationSelect() {
56
+ return this.routeOptimization.availableEngines.length > 1;
57
+ }
58
+
59
+ @task *optimizeRouteWithService(service) {
60
+ const order = this.args.resource;
61
+ const optimizationInput = buildRouteOptimizationInput(order);
62
+
63
+ if (!service) {
64
+ this.notifications.error('No route optimization engine was selected.');
65
+ return;
66
+ }
67
+
68
+ try {
69
+ const result = yield this.routeOptimization.optimize(service, {
70
+ context: 'order_details_route',
71
+ ...optimizationInput,
72
+ });
73
+
74
+ yield this.persistOptimizedRoute(result);
75
+ } catch (err) {
76
+ this.notifications.error(err.message ?? this.intl.t('fleet-ops.operations.orders.index.new.route-error'));
77
+ }
78
+ }
79
+
80
+ @task *optimizeRoute() {
81
+ const order = this.args.resource;
82
+ const optimizationInput = buildRouteOptimizationInput(order);
83
+ const service = this.routeEngine.getOptimizationEngine('osrm');
84
+
85
+ try {
86
+ const result = yield this.routeOptimization.optimize(service, {
87
+ context: 'order_details_route',
88
+ ...optimizationInput,
89
+ });
90
+
91
+ yield this.persistOptimizedRoute(result);
92
+ } catch (_err) {
93
+ this.notifications.error(this.intl.t('fleet-ops.operations.orders.index.new.route-error'));
94
+ }
95
+ }
96
+
97
+ *persistOptimizedRoute({ sortedWaypoints }) {
98
+ applyOptimizedIntermediateWaypoints(this.args.resource.payload, sortedWaypoints);
99
+ yield this.orderActions.saveRoute.perform(this.args.resource);
100
+ yield this.hostRouter.refresh();
101
+ }
102
+
27
103
  @action async viewWaypointLabel(waypoint) {
28
- // render dialog to display label within
29
104
  this.modalsManager.show(`modals/order-label`, {
30
105
  title: 'Waypoint Label',
31
106
  modalClass: 'modal-xl',
@@ -34,14 +109,12 @@ export default class OrderDetailsRouteComponent extends Component {
34
109
  });
35
110
 
36
111
  try {
37
- // load the pdf label from base64
38
112
  // eslint-disable-next-line no-undef
39
113
  const fileReader = new FileReader();
40
114
  const { data: pdfStream } = await this.fetch.get(`orders/label/${waypoint.waypoint_public_id}?format=base64`);
41
115
  // eslint-disable-next-line no-undef
42
116
  const base64 = await fetch(`data:application/pdf;base64,${pdfStream}`);
43
117
  const blob = await base64.blob();
44
- // load into file reader
45
118
  fileReader.onload = (event) => {
46
119
  const data = event.target.result;
47
120
  this.modalsManager.setOption('data', data);
@@ -71,6 +71,24 @@
71
71
  <div>
72
72
  <Input @value={{entity.sku}} @type="text" class="w-full form-input form-input-sm" placeholder={{t "order.fields.sku"}} />
73
73
  </div>
74
+ <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
75
+ <PowerSelect
76
+ @options={{get-fleet-ops-options "entityTypes"}}
77
+ @selected={{find-by "value" entity.type (get-fleet-ops-options "entityTypes")}}
78
+ @onChange={{set-model-attr entity "type"}}
79
+ @placeholder="Select entity type"
80
+ @triggerClass="form-select"
81
+ as |type|
82
+ >
83
+ <div class="text-sm">
84
+ <div class="font-semibold normalize-in-trigger">{{type.label}}</div>
85
+ <div class="hide-from-trigger">{{type.description}}</div>
86
+ </div>
87
+ </PowerSelect>
88
+ </div>
89
+ <RegistryYield @registry="fleet-ops:component:order:form:payload:entity:form" as |RegistryComponent|>
90
+ <RegistryComponent @entity={{entity}} @order={{@resource}} @controller={{this}} />
91
+ </RegistryYield>
74
92
  <Button @icon="pencil" @text={{t "order.fields.edit-item"}} @size="sm" @onClick={{fn this.editEntity entity}} />
75
93
  </div>
76
94
  <div class="absolute top-0 right-0 p-2">
@@ -105,6 +123,21 @@
105
123
  <div>
106
124
  <Input @value={{entity.sku}} @type="text" class="w-full form-input form-input-sm" placeholder={{t "order.fields.sku"}} />
107
125
  </div>
126
+ <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
127
+ <PowerSelect
128
+ @options={{get-fleet-ops-options "entityTypes"}}
129
+ @selected={{find-by "value" entity.type (get-fleet-ops-options "entityTypes")}}
130
+ @onChange={{set-model-attr entity "type"}}
131
+ @placeholder="Select entity type"
132
+ @triggerClass="form-select"
133
+ as |type|
134
+ >
135
+ <div class="text-sm">
136
+ <div class="font-semibold normalize-in-trigger">{{type.label}}</div>
137
+ <div class="hide-from-trigger">{{type.description}}</div>
138
+ </div>
139
+ </PowerSelect>
140
+ </div>
108
141
  <RegistryYield @registry="fleet-ops:component:order:form:payload:entity:form" as |RegistryComponent|>
109
142
  <RegistryComponent @entity={{entity}} @order={{@resource}} @controller={{this}} />
110
143
  </RegistryYield>
@@ -52,6 +52,7 @@ export default class OrderFormPayloadComponent extends Component {
52
52
  @action addEntity(importId = null) {
53
53
  const entity = this.store.createRecord('entity', {
54
54
  _import_id: typeof importId === 'string' ? importId : null,
55
+ type: 'entity',
55
56
  });
56
57
 
57
58
  this.args.resource.payload.entities.pushObject(entity);
@@ -56,9 +56,9 @@
56
56
  @renderInPlace={{true}}
57
57
  @onChange={{fn this.setWaypointPlace index}}
58
58
  @permission={{cannot-write @resource}}
59
- as |model|
59
+ as |place|
60
60
  >
61
- {{model.address}}
61
+ <Place::Address @place={{place}} />
62
62
  </ModelSelect>
63
63
  {{#if (and @resource.facilitator.isIntegratedVendor (is-not-facilitator-supported-place @resource.facilitator waypoint.place))}}
64
64
  <InfoBlock
@@ -169,9 +169,9 @@
169
169
  @renderInPlace={{true}}
170
170
  @onChange={{fn this.setPayloadPlace "pickup"}}
171
171
  @permission={{cannot-write @resource}}
172
- as |model|
172
+ as |place|
173
173
  >
174
- {{model.address}}
174
+ <Place::Address @place={{place}} />
175
175
  </ModelSelect>
176
176
 
177
177
  {{#if (and @resource.facilitator.isIntegratedVendor (is-not-facilitator-supported-place @resource.facilitator @resource.payload.pickup))}}
@@ -213,9 +213,9 @@
213
213
  @renderInPlace={{true}}
214
214
  @onChange={{fn this.setPayloadPlace "dropoff"}}
215
215
  @permission={{cannot-write @resource}}
216
- as |model|
216
+ as |place|
217
217
  >
218
- {{model.address}}
218
+ <Place::Address @place={{place}} />
219
219
  </ModelSelect>
220
220
 
221
221
  {{#if (and @resource.facilitator.isIntegratedVendor (is-not-facilitator-supported-place @resource.facilitator @resource.payload.dropoff))}}
@@ -250,9 +250,9 @@
250
250
  @renderInPlace={{true}}
251
251
  @onChange={{fn this.setPayloadPlace "return"}}
252
252
  @permission={{cannot-write @resource}}
253
- as |model|
253
+ as |place|
254
254
  >
255
- {{model.address}}
255
+ <Place::Address @place={{place}} />
256
256
  </ModelSelect>
257
257
 
258
258
  {{#if (and @resource.facilitator.isIntegratedVendor (is-not-facilitator-supported-place @resource.facilitator @resource.payload.return))}}