@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
@@ -3,11 +3,18 @@ import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { action, get } from '@ember/object';
5
5
  import { task } from 'ember-concurrency';
6
+ import { colorForId, routeColorForStatus, routeStyleForStatus } from '../../../utils/route-colors';
7
+ import { buildRoutePointMarkerPresentation, buildRoutePointsFromPayload } from '../../../utils/route-visualization';
8
+
9
+ const ORDER_ROUTE_PREVIEW_PADDING_BOTTOM_RIGHT = [420, 0];
10
+ const ORDER_ROUTE_PREVIEW_MAX_ZOOM_TWO_POINTS = 13;
11
+ const ORDER_ROUTE_PREVIEW_MAX_ZOOM_MULTI_POINTS = 12;
12
+ const ORDER_ROUTE_PREVIEW_SINGLE_POINT_PANBY = [10, 0];
6
13
 
7
14
  export default class OrderFormRouteComponent extends Component {
8
- @service leafletMapManager;
15
+ @service mapManager;
16
+ @service routeEngine;
9
17
  @service routeOptimization;
10
- @service osrm;
11
18
  @service location;
12
19
  @service store;
13
20
  @service currentUser;
@@ -17,24 +24,31 @@ export default class OrderFormRouteComponent extends Component {
17
24
  @tracked routingControl;
18
25
  @tracked route;
19
26
 
20
- get coordinates() {
21
- const payload = this.args.resource.payload;
22
- const waypoints = payload ? [payload.pickup, ...this.args.resource.payload.waypoints.map((waypoint) => waypoint.place), payload.dropoff] : [];
27
+ focusPlace(place, zoom = 18) {
28
+ if (place?.hasValidCoordinates) {
29
+ this.mapManager.positionWaypoints([[place.latitude, place.longitude]], {
30
+ singlePointZoom: zoom,
31
+ panBy: ORDER_ROUTE_PREVIEW_SINGLE_POINT_PANBY,
32
+ });
33
+ }
34
+ }
23
35
 
24
- return waypoints.filter((wp) => wp && wp.hasValidCoordinates).map((wp) => [wp.latitude, wp.longitude]);
36
+ get coordinates() {
37
+ return this.routePoints.map(({ place }) => [place.latitude, place.longitude]);
25
38
  }
26
39
 
27
40
  get places() {
28
- const payload = this.args.resource.payload;
29
- const waypoints = payload ? [payload.pickup, ...this.args.resource.payload.waypoints.map((waypoint) => waypoint.place), payload.dropoff] : [];
41
+ return this.routePoints.map(({ place }) => place);
42
+ }
30
43
 
31
- return waypoints.filter((place) => place);
44
+ get routePoints() {
45
+ return buildRoutePointsFromPayload(this.args.resource.payload);
32
46
  }
33
47
 
34
48
  willDestroy() {
35
49
  super.willDestroy(...arguments);
36
50
  if (this.routingControl) {
37
- this.leafletMapManager.removeRoutingControl(this.routingControl);
51
+ this.mapManager.removeRoutingControl(this.routingControl);
38
52
  }
39
53
  }
40
54
 
@@ -91,15 +105,15 @@ export default class OrderFormRouteComponent extends Component {
91
105
 
92
106
  const waypoint = this.store.createRecord('waypoint', { ...properties, type: 'dropoff' });
93
107
  this.args.resource.payload.waypoints.pushObject(waypoint);
108
+
94
109
  this.previewRoute();
95
110
  }
96
111
 
97
112
  @action setWaypointPlace(index, place) {
98
- if (!this.args.resource.payload.waypoints[index]) {
99
- return;
100
- }
113
+ if (!this.args.resource.payload.waypoints[index]) return;
101
114
 
102
115
  this.args.resource.payload.waypoints[index].place = place;
116
+ this.args.resource.payload.waypoints[index]?.setProperties(place.serialize());
103
117
  this.previewRoute();
104
118
  }
105
119
 
@@ -129,13 +143,55 @@ export default class OrderFormRouteComponent extends Component {
129
143
  }
130
144
 
131
145
  @action async previewRoute() {
132
- if (!this.coordinates.length) return;
146
+ if (!this.coordinates.length) {
147
+ if (this.routingControl) {
148
+ this.mapManager.removeRoutingControl(this.routingControl);
149
+ this.routingControl = null;
150
+ }
133
151
 
134
- const routingControl = await this.leafletMapManager.replaceRoutingControl(this.coordinates, this.routingControl, {
152
+ return;
153
+ }
154
+
155
+ const order = this.args.resource;
156
+ const orderId = order.public_id ?? order.id ?? 'new-order';
157
+ const routeColor = colorForId(orderId);
158
+ const routeStatus = order.status ?? 'created';
159
+ const statusColor = routeColorForStatus(routeStatus);
160
+ const routeStyles = routeStyleForStatus(routeStatus, statusColor);
161
+ const isSinglePointPreview = this.coordinates.length === 1;
162
+ const fitOptions = isSinglePointPreview
163
+ ? {
164
+ paddingBottomRight: [0, 0],
165
+ panBy: ORDER_ROUTE_PREVIEW_SINGLE_POINT_PANBY,
166
+ }
167
+ : {
168
+ paddingBottomRight: ORDER_ROUTE_PREVIEW_PADDING_BOTTOM_RIGHT,
169
+ panBy: [0, 0],
170
+ maxZoom: this.coordinates.length === 2 ? ORDER_ROUTE_PREVIEW_MAX_ZOOM_TWO_POINTS : ORDER_ROUTE_PREVIEW_MAX_ZOOM_MULTI_POINTS,
171
+ };
172
+ const routingControl = await this.mapManager.replaceRoutingControl(this.coordinates, this.routingControl, {
173
+ engine: this.routeEngine.getDisplayEngine('osrm'),
174
+ color: statusColor,
175
+ orderId,
176
+ status: routeStatus,
177
+ fitOptions,
178
+ places: this.places,
179
+ markerWaypoints: this.coordinates,
180
+ polylineOptions: {
181
+ color: statusColor,
182
+ weight: routeStyles.at(-1)?.weight ?? 4,
183
+ opacity: routeStyles.at(-1)?.opacity ?? 0.85,
184
+ styles: routeStyles,
185
+ },
186
+ createMarker: (_waypoint, index) => {
187
+ const routePoint = this.routePoints[index];
188
+ return buildRoutePointMarkerPresentation(routePoint, routeColor);
189
+ },
135
190
  onRouteFound: (route) => this.setRoute(route),
136
191
  removeOptions: {
137
- filter: (layer) => layer.record_id === this.args.resource.driver_assigned?.id,
192
+ filter: (handle) => handle?.tag === this.args.resource.driver_assigned?.id,
138
193
  },
194
+ tag: this.args.resource.driver_assigned?.id,
139
195
  });
140
196
 
141
197
  this.routingControl = routingControl;
@@ -166,9 +222,10 @@ export default class OrderFormRouteComponent extends Component {
166
222
  const payload = order.payload;
167
223
  const waypoints = this.args.resource.payload.waypoints;
168
224
  const coordinates = this.coordinates.map((coord) => coord.reverse());
225
+ const service = this.routeEngine.getOptimizationEngine('osrm');
169
226
 
170
227
  try {
171
- const result = yield this.osrm.optimize({
228
+ const result = yield this.routeOptimization.optimize(service, {
172
229
  context: 'create_order',
173
230
  order,
174
231
  payload,
@@ -193,7 +250,10 @@ export default class OrderFormRouteComponent extends Component {
193
250
  }
194
251
 
195
252
  @action setOptimizedRoute(route, trip, waypoints, engine = 'osrm') {
196
- let summary = { totalDistance: trip.distance, totalTime: trip.duration };
253
+ let summary = {
254
+ totalDistance: trip?.distance ?? trip?.totalDistance ?? 0,
255
+ totalTime: trip?.duration ?? trip?.totalTime ?? 0,
256
+ };
197
257
  let payload = {
198
258
  optimized: true,
199
259
  coordinates: route,
@@ -58,7 +58,7 @@
58
58
  @name="serviceQuote"
59
59
  @changed={{fn (mut @resource.service_quote_uuid)}}
60
60
  />
61
- <label for={{serviceQuote.uuid}} class="ml-3 flex-1">{{serviceQuote.public_id}}</label>
61
+ <label for={{serviceQuote.uuid}} class="ml-3 flex-1 text-sm">{{serviceQuote.public_id}} ({{serviceQuote.service_rate_name}})</label>
62
62
  <Badge @hideStatusDot={{true}} @status="info">
63
63
  {{serviceQuote.request_id}}
64
64
  </Badge>
@@ -9,7 +9,7 @@ export default class OrderFormComponent extends Component {
9
9
  @service store;
10
10
  @service orderConfigActions;
11
11
  @service customFieldsRegistry;
12
- @service leafletMapManager;
12
+ @service mapManager;
13
13
  @service currentUser;
14
14
  @tracked customFields;
15
15
 
@@ -47,7 +47,7 @@ export default class OrderFormComponent extends Component {
47
47
  debug('Unable to load and set driver vehicle: ' + err.message);
48
48
  }
49
49
 
50
- this.leafletMapManager.map.liveMap.focusDriver(driver);
50
+ this.mapManager.focusResource(driver, 18);
51
51
  // if (this.args.resource.is_route_optimized) {
52
52
  // this.optimizeRoute.perform();
53
53
  // }
@@ -1,171 +1,233 @@
1
1
  <div class="order-route-editor" ...attributes>
2
- <div class="flex flex-row justify-between mb-6">
3
- <div class="flex">
4
- <Toggle @isToggled={{@resource.payload.isMultiDrop}} @onToggle={{this.toggleMultiDropOrder}} @label={{t "modals.order-route-form.toggle"}} />
2
+ <div class="flex flex-row items-start justify-between mb-4">
3
+ <div>
4
+ <div class="text-sm font-semibold dark:text-gray-100">Route Structure</div>
5
+ <div class="text-xs text-gray-400 mt-1">{{this.routeSummary}}</div>
5
6
  </div>
6
- <div class="flex flex-1 justify-end space-x-2">
7
- {{#if @resource.payload.isMultiDrop}}
8
- {{#if this.routeOptimization.availableEngines}}
9
- <RouteOptimizationEngineSelectButton @onClick={{perform this.optimizeRouteWithService}} @isLoading={{this.optimizeRouteWithService.isRunning}} @size="sm" />
10
- {{else}}
11
- <Button
12
- @type="magic"
13
- @icon="magic"
14
- @text={{t "order.fields.optimize-route"}}
15
- @size="sm"
16
- @onClick={{perform this.optimizeRoute}}
17
- @permission="fleet-ops optimize order"
18
- @helpText={{t "order.fields.optimize-route-help-text"}}
19
- @disabled={{lt @resource.payload.waypoints.length 3}}
20
- @isLoading={{this.optimizeRoute.isRunning}}
21
- />
22
- {{/if}}
23
- <Button @icon="map-marked-alt" @text={{t "modals.order-route-form.add"}} @size="sm" @onClick={{this.addWaypoint}} @helpText={{t "modals.order-route-form.add-text"}} />
7
+
8
+ <div class="flex flex-wrap justify-end gap-2">
9
+ {{#if (gt this.routeOptimization.availableEngines.length 1)}}
10
+ <RouteOptimizationEngineSelectButton
11
+ @onClick={{perform this.optimizeRouteWithService}}
12
+ @isLoading={{this.optimizeRouteWithService.isRunning}}
13
+ @size="sm"
14
+ @disabled={{not this.canOptimizeRoute}}
15
+ />
16
+ {{else}}
17
+ <Button
18
+ @type="magic"
19
+ @icon="magic"
20
+ @text={{t "order.fields.optimize-route"}}
21
+ @size="sm"
22
+ @onClick={{perform this.optimizeRoute}}
23
+ @permission="fleet-ops optimize order"
24
+ @helpText={{t "order.fields.optimize-route-help-text"}}
25
+ @disabled={{not this.canOptimizeRoute}}
26
+ @isLoading={{this.optimizeRoute.isRunning}}
27
+ />
24
28
  {{/if}}
29
+
30
+ <Button @icon="map-marked-alt" @text={{t "modals.order-route-form.add"}} @size="sm" @onClick={{this.addWaypoint}} @helpText={{t "modals.order-route-form.add-text"}} />
25
31
  </div>
26
32
  </div>
27
33
 
28
- {{#if @resource.payload.isMultiDrop}}
29
- <DragSortList class="overflow-visible" @items={{@resource.payload.waypoints}} @dragEndAction={{this.sortWaypoints}} as |waypoint index|>
30
- <div id={{concat "waypoint_" (add index 1)}} class="py-1 mb-3">
31
- <div class="flex items-center justify-between">
32
- <div class="flex items-center justify-between cursor-move">
33
- <FaIcon @icon="grip-lines" @prefix="fas" class="mr-3 text-gray-100" />
34
- <label class="waypoint-label dark:text-gray-100 text-sm">
35
- {{waypoint-label (add index 1)}}
36
- </label>
37
- </div>
38
-
39
- <div class="flex-1 px-4">
40
- <ModelSelect
41
- @modelName="place"
42
- @selectedModel={{waypoint}}
43
- @placeholder={{concat (t "order.fields.select-waypoint") " " (add index 1)}}
44
- @triggerClass="form-select form-input truncate max-w-300px"
45
- @infiniteScroll={{false}}
46
- @customSearchEndpoint="places/search"
47
- @query={{hash geo=true latitude=this.latitude longitude=this.longitude}}
48
- @renderInPlace={{true}}
49
- @onChange={{fn this.setWaypointPlace index}}
50
- as |model|
51
- >
52
- {{model.address}}
53
- </ModelSelect>
54
-
55
- {{#unless waypoint.isNew}}
56
- {{#if waypoint.hasInvalidCoordinates}}
57
- <div class="leading-5 text-sm text-red-400 mt-2">
58
- <FaIcon @icon="exclamation-triangle" class="mr-1" />
59
- {{t "modals.order-route-form.invalid"}}
60
- </div>
61
- {{/if}}
62
- {{/unless}}
63
- </div>
64
-
65
- <div class="flex items-center space-x-2">
66
- <Button @icon="edit" @size="sm" @onClick={{fn this.placeActions.modal.edit waypoint}} />
67
- <Button @type="danger" @icon="trash" @iconPrefix="fas" @size="sm" @onClick={{fn this.removeWaypoint waypoint}} />
68
- </div>
34
+ <div class="fleetops-route-editor-section mb-3">
35
+ <div class="fleetops-route-editor-section__header">
36
+ <div class="fleetops-route-editor-section__title-group">
37
+ <span class="fleetops-route-stop-badge fleetops-route-editor-section__badge" style={{this.pickupStop.badgeStyle}}>{{this.pickupStop.label}}</span>
38
+ <div>
39
+ <div class="fleetops-route-editor-section__title">Pickup</div>
40
+ <div class="fleetops-route-editor-section__subtitle">Route starting location</div>
69
41
  </div>
70
42
  </div>
71
- </DragSortList>
72
- {{else}}
73
- <div class="grid grid-cols-1 gap-3 text-xs dark:text-gray-100">
74
- <div class="input-group">
75
- <div class="flex items-center justify-between mb-2">
76
- <label class="mb-0i">{{t "modals.order-route-form.pickup"}}</label>
77
-
78
- {{#if @resource.payload.pickup}}
79
- <Button @text={{t "modals.order-route-form.edit-address"}} @icon="edit" @size="xs" @onClick={{fn this.placeActions.modal.edit @resource.payload.pickup}} />
80
- {{/if}}
81
- </div>
82
43
 
83
- <ModelSelect
84
- @modelName="place"
85
- @selectedModel={{@resource.payload.pickup}}
86
- @placeholder={{t "modals.order-route-form.select-pickup"}}
87
- @triggerClass="form-select form-input"
88
- @infiniteScroll={{false}}
89
- @customSearchEndpoint="places/search"
90
- @query={{hash geo=true latitude=this.latitude longitude=this.longitude}}
91
- @renderInPlace={{true}}
92
- @onChange={{fn this.setPayloadPlace "pickup"}}
93
- as |model|
94
- >
95
- {{model.address}}
96
- </ModelSelect>
97
-
98
- {{#if @resource.payload.pickup.hasInvalidCoordinates}}
99
- <div class="leading-5 text-sm text-red-400 mt-2">
100
- <FaIcon @icon="exclamation-triangle" class="mr-1" />
101
- {{t "modals.order-route-form.invalid"}}
102
- </div>
44
+ <div class="flex items-center gap-2">
45
+ {{#if @resource.payload.pickup}}
46
+ <Button @text={{t "modals.order-route-form.edit-address"}} @icon="edit" @size="xs" @onClick={{fn this.placeActions.modal.edit @resource.payload.pickup}} />
47
+ <Button @type="danger" @icon="trash" @iconPrefix="fas" @size="xs" @onClick={{fn this.clearPayloadPlace "pickup"}} />
103
48
  {{/if}}
104
49
  </div>
50
+ </div>
105
51
 
106
- <div class="input-group">
107
- <div class="flex items-center justify-between mb-2">
108
- <label class="mb-0i">{{t "modals.order-route-form.dropoff"}}</label>
52
+ <div class="fleetops-route-editor-section__body">
53
+ <ModelSelect
54
+ @modelName="place"
55
+ @selectedModel={{@resource.payload.pickup}}
56
+ @placeholder={{t "modals.order-route-form.select-pickup"}}
57
+ @triggerClass="form-select form-input"
58
+ @infiniteScroll={{false}}
59
+ @customSearchEndpoint="places/search"
60
+ @renderInPlace={{true}}
61
+ @onChange={{fn this.setPayloadPlace "pickup"}}
62
+ as |place|
63
+ >
64
+ <Place::Address @place={{place}} />
65
+ </ModelSelect>
66
+
67
+ {{#if @resource.payload.pickup.hasInvalidCoordinates}}
68
+ <div class="leading-5 text-sm text-red-400 mt-2">
69
+ <FaIcon @icon="exclamation-triangle" class="mr-1" />
70
+ {{t "modals.order-route-form.invalid"}}
71
+ </div>
72
+ {{/if}}
73
+ </div>
74
+ </div>
109
75
 
110
- {{#if @resource.payload.dropoff}}
111
- <Button @text={{t "modals.order-route-form.edit-address"}} @icon="edit" @size="xs" @onClick={{fn this.placeActions.modal.edit @resource.payload.dropoff}} />
112
- {{/if}}
76
+ <div class="fleetops-route-editor-section mb-3">
77
+ <div class="fleetops-route-editor-section__header">
78
+ <div class="fleetops-route-editor-section__title-group">
79
+ <span class="fleetops-route-stop-badge fleetops-route-editor-section__badge" style={{this.stopsSectionBadgeStyle}}>{{this.intermediateWaypoints.length}}</span>
80
+ <div>
81
+ <div class="fleetops-route-editor-section__title">Intermediate Stops</div>
82
+ <div class="fleetops-route-editor-section__subtitle">Drag to reorder stops between the fixed endpoints</div>
113
83
  </div>
84
+ </div>
85
+
86
+ <div class="text-xs text-gray-400">{{this.intermediateWaypoints.length}} configured</div>
87
+ </div>
88
+
89
+ <div class="fleetops-route-editor-section__body">
90
+ {{#if this.intermediateWaypoints.length}}
91
+ <DragSortList class="overflow-visible" @items={{this.intermediateWaypoints}} @dragEndAction={{this.sortWaypoints}} as |waypoint index|>
92
+ {{#let (get this.intermediateRouteStops index) as |stop|}}
93
+ <div id={{concat "waypoint_" (add index 1)}} class="fleetops-route-editor-stop-row">
94
+ <div class="fleetops-route-editor-stop-row__grip cursor-move">
95
+ <FaIcon @icon="grip-lines" @prefix="fas" class="text-gray-400" />
96
+ </div>
97
+
98
+ <div class="fleetops-route-editor-stop-row__label">
99
+ <span class="fleetops-route-stop-badge fleetops-route-editor-stop-row__badge" style={{stop.badgeStyle}}>{{stop.label}}</span>
100
+ <span class="fleetops-route-editor-stop-row__title">{{stop.title}}</span>
101
+ </div>
114
102
 
115
- <ModelSelect
116
- @modelName="place"
117
- @selectedModel={{@resource.payload.dropoff}}
118
- @placeholder={{t "modals.order-route-form.select-dropoff"}}
119
- @triggerClass="form-select form-input"
120
- @infiniteScroll={{false}}
121
- @customSearchEndpoint="places/search"
122
- @query={{hash geo=true latitude=this.latitude longitude=this.longitude}}
123
- @renderInPlace={{true}}
124
- @onChange={{fn this.setPayloadPlace "dropoff"}}
125
- as |model|
126
- >
127
- {{model.address}}
128
- </ModelSelect>
129
-
130
- {{#if @resource.payload.dropoff.hasInvalidCoordinates}}
131
- <div class="leading-5 text-sm text-red-400 mt-2">
132
- <FaIcon @icon="exclamation-triangle" class="mr-1" />
133
- {{t "modals.order-route-form.invalid"}}
134
- </div>
103
+ <div class="fleetops-route-editor-stop-row__field">
104
+ {{#let (or waypoint.place waypoint) as |waypointPlace|}}
105
+ <ModelSelect
106
+ @modelName="place"
107
+ @selectedModel={{waypointPlace}}
108
+ @placeholder={{concat (t "order.fields.select-waypoint") " " (add index 1)}}
109
+ @triggerClass="form-select form-input truncate"
110
+ @infiniteScroll={{false}}
111
+ @customSearchEndpoint="places/search"
112
+ @renderInPlace={{true}}
113
+ @onChange={{fn this.setWaypointPlace index}}
114
+ as |place|
115
+ >
116
+ <Place::Address @place={{place}} />
117
+ </ModelSelect>
118
+
119
+ {{#if waypointPlace.hasInvalidCoordinates}}
120
+ <div class="leading-5 text-sm text-red-400 mt-2">
121
+ <FaIcon @icon="exclamation-triangle" class="mr-1" />
122
+ {{t "modals.order-route-form.invalid"}}
123
+ </div>
124
+ {{/if}}
125
+ {{/let}}
126
+ </div>
127
+
128
+ <div class="fleetops-route-editor-stop-row__actions">
129
+ {{#let (or waypoint.place waypoint) as |waypointPlace|}}
130
+ {{#if waypointPlace}}
131
+ <Button @icon="edit" @size="sm" @onClick={{fn this.placeActions.modal.edit waypointPlace}} />
132
+ {{/if}}
133
+ {{/let}}
134
+ <Button @type="danger" @icon="trash" @iconPrefix="fas" @size="sm" @onClick={{fn this.removeWaypoint waypoint}} />
135
+ </div>
136
+ </div>
137
+ {{/let}}
138
+ </DragSortList>
139
+ {{else}}
140
+ <div class="fleetops-route-editor-empty-state">
141
+ <div class="text-sm dark:text-gray-100">No intermediate stops yet.</div>
142
+ <div class="text-xs text-gray-400 mt-1">Add a stop to build a mixed route between pickup and dropoff.</div>
143
+ </div>
144
+ {{/if}}
145
+ </div>
146
+ </div>
147
+
148
+ <div class="fleetops-route-editor-section mb-3">
149
+ <div class="fleetops-route-editor-section__header">
150
+ <div class="fleetops-route-editor-section__title-group">
151
+ <span class="fleetops-route-stop-badge fleetops-route-editor-section__badge" style={{this.dropoffStop.badgeStyle}}>{{this.dropoffStop.label}}</span>
152
+ <div>
153
+ <div class="fleetops-route-editor-section__title">Dropoff</div>
154
+ <div class="fleetops-route-editor-section__subtitle">Route ending location</div>
155
+ </div>
156
+ </div>
157
+
158
+ <div class="flex items-center gap-2">
159
+ {{#if @resource.payload.dropoff}}
160
+ <Button @text={{t "modals.order-route-form.edit-address"}} @icon="edit" @size="xs" @onClick={{fn this.placeActions.modal.edit @resource.payload.dropoff}} />
161
+ <Button @type="danger" @icon="trash" @iconPrefix="fas" @size="xs" @onClick={{fn this.clearPayloadPlace "dropoff"}} />
135
162
  {{/if}}
136
163
  </div>
164
+ </div>
137
165
 
138
- <div class="input-group">
139
- <div class="flex items-center justify-between mb-2">
140
- <label class="mb-0i">{{t "modals.order-route-form.return"}}</label>
166
+ <div class="fleetops-route-editor-section__body">
167
+ <ModelSelect
168
+ @modelName="place"
169
+ @selectedModel={{@resource.payload.dropoff}}
170
+ @placeholder={{t "modals.order-route-form.select-dropoff"}}
171
+ @triggerClass="form-select form-input"
172
+ @infiniteScroll={{false}}
173
+ @customSearchEndpoint="places/search"
174
+ @renderInPlace={{true}}
175
+ @onChange={{fn this.setPayloadPlace "dropoff"}}
176
+ as |place|
177
+ >
178
+ <Place::Address @place={{place}} />
179
+ </ModelSelect>
141
180
 
142
- {{#if @resource.payload.return}}
143
- <Button @text={{t "modals.order-route-form.edit-address"}} @icon="edit" @size="xs" @onClick={{fn this.placeActions.modal.edit @resource.payload.return}} />
144
- {{/if}}
181
+ {{#if @resource.payload.dropoff.hasInvalidCoordinates}}
182
+ <div class="leading-5 text-sm text-red-400 mt-2">
183
+ <FaIcon @icon="exclamation-triangle" class="mr-1" />
184
+ {{t "modals.order-route-form.invalid"}}
145
185
  </div>
186
+ {{/if}}
187
+ </div>
188
+ </div>
146
189
 
147
- <ModelSelect
148
- @modelName="place"
149
- @selectedModel={{@resource.payload.return}}
150
- @placeholder={{t "modals.order-route-form.select-return"}}
151
- @triggerClass="form-select form-input"
152
- @infiniteScroll={{false}}
153
- @customSearchEndpoint="places/search"
154
- @query={{hash geo=true latitude=this.latitude longitude=this.longitude}}
155
- @renderInPlace={{true}}
156
- @onChange={{fn this.setPayloadPlace "return"}}
157
- as |model|
158
- >
159
- {{model.address}}
160
- </ModelSelect>
161
-
162
- {{#if @resource.payload.return.hasInvalidCoordinates}}
163
- <div class="leading-5 text-sm text-red-400 mt-2">
164
- <FaIcon @icon="exclamation-triangle" class="mr-1" />
165
- {{t "modals.order-route-form.invalid"}}
166
- </div>
190
+ <div class="fleetops-route-editor-section">
191
+ <div class="fleetops-route-editor-section__header">
192
+ <div class="fleetops-route-editor-section__title-group">
193
+ <span class="fleetops-route-stop-badge fleetops-route-editor-section__badge" style={{this.returnStop.badgeStyle}}>{{this.returnStop.label}}</span>
194
+ <div>
195
+ <div class="fleetops-route-editor-section__title">Return</div>
196
+ <div class="fleetops-route-editor-section__subtitle">Optional fallback destination</div>
197
+ </div>
198
+ </div>
199
+
200
+ <div class="flex items-center gap-2">
201
+ {{#if @resource.payload.return}}
202
+ <Button @text={{t "modals.order-route-form.edit-address"}} @icon="edit" @size="xs" @onClick={{fn this.placeActions.modal.edit @resource.payload.return}} />
203
+ <Button @type="danger" @icon="trash" @iconPrefix="fas" @size="xs" @onClick={{fn this.clearPayloadPlace "return"}} />
167
204
  {{/if}}
168
205
  </div>
169
206
  </div>
170
- {{/if}}
171
- </div>
207
+
208
+ <div class="fleetops-route-editor-section__body">
209
+ <ModelSelect
210
+ @modelName="place"
211
+ @selectedModel={{@resource.payload.return}}
212
+ @placeholder={{t "modals.order-route-form.select-return"}}
213
+ @triggerClass="form-select form-input"
214
+ @infiniteScroll={{false}}
215
+ @customSearchEndpoint="places/search"
216
+ @renderInPlace={{true}}
217
+ @onChange={{fn this.setPayloadPlace "return"}}
218
+ as |place|
219
+ >
220
+ <Place::Address @place={{place}} />
221
+ </ModelSelect>
222
+
223
+ {{#if @resource.payload.return.hasInvalidCoordinates}}
224
+ <div class="leading-5 text-sm text-red-400 mt-2">
225
+ <FaIcon @icon="exclamation-triangle" class="mr-1" />
226
+ {{t "modals.order-route-form.invalid"}}
227
+ </div>
228
+ {{/if}}
229
+ </div>
230
+ </div>
231
+
232
+ <Spacer @height="160px" />
233
+ </div>