@fleetbase/fleetops-engine 0.6.45 → 0.6.47

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 (148) 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/layout/fleet-ops-sidebar/driver-listing.js +5 -5
  10. package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +5 -5
  11. package/addon/components/layout/fleet-ops-sidebar.js +8 -0
  12. package/addon/components/maintenance-schedule/form.js +2 -1
  13. package/addon/components/map/drawer/driver-listing.js +5 -5
  14. package/addon/components/map/drawer/place-listing.js +5 -5
  15. package/addon/components/map/drawer/position-listing.js +102 -21
  16. package/addon/components/map/drawer/vehicle-listing.js +5 -5
  17. package/addon/components/map/google-live-map.hbs +9 -0
  18. package/addon/components/map/google-live-map.js +445 -0
  19. package/addon/components/map/leaflet-live-map.hbs +186 -142
  20. package/addon/components/map/leaflet-live-map.js +121 -41
  21. package/addon/components/map/toolbar/visibility-control-panel.js +2 -2
  22. package/addon/components/map/toolbar/zones-panel.hbs +9 -9
  23. package/addon/components/map/toolbar.hbs +3 -3
  24. package/addon/components/map/toolbar.js +9 -1
  25. package/addon/components/modals/map-layer-form.hbs +132 -63
  26. package/addon/components/modals/orchestrator-import.hbs +4 -11
  27. package/addon/components/modals/orchestrator-import.js +17 -1
  28. package/addon/components/modals/update-order-activity.hbs +1 -1
  29. package/addon/components/orchestrator/order-pool.js +3 -2
  30. package/addon/components/orchestrator-workbench.js +29 -41
  31. package/addon/components/order/details/detail.js +2 -2
  32. package/addon/components/order/details/payload.hbs +5 -5
  33. package/addon/components/order/details/payload.js +18 -1
  34. package/addon/components/order/details/route.hbs +2 -2
  35. package/addon/components/order/details/route.js +76 -3
  36. package/addon/components/order/form/details.hbs +1 -2
  37. package/addon/components/order/form/details.js +6 -0
  38. package/addon/components/order/form/payload.hbs +33 -0
  39. package/addon/components/order/form/payload.js +1 -0
  40. package/addon/components/order/form/route.hbs +8 -8
  41. package/addon/components/order/form/route.js +86 -18
  42. package/addon/components/order/form.js +2 -2
  43. package/addon/components/order/route-editor.hbs +209 -147
  44. package/addon/components/order/route-editor.js +143 -102
  45. package/addon/components/place/address.hbs +1 -0
  46. package/addon/components/place/address.js +12 -0
  47. package/addon/components/route-list.hbs +43 -55
  48. package/addon/components/route-list.js +47 -0
  49. package/addon/components/route-optimization-wizard-panel.js +3 -4
  50. package/addon/components/vehicle/card.hbs +4 -4
  51. package/addon/controllers/maintenance/schedules/index.js +2 -1
  52. package/addon/controllers/operations/orders/index/details.js +110 -37
  53. package/addon/controllers/operations/orders/index/new.js +1 -1
  54. package/addon/controllers/operations/orders/index.js +8 -1
  55. package/addon/controllers/settings/map.js +97 -0
  56. package/addon/controllers/settings/payments/index.js +1 -1
  57. package/addon/controllers/settings/routing.js +47 -12
  58. package/addon/extension.js +11 -0
  59. package/addon/helpers/place-address-html.js +6 -0
  60. package/addon/instance-initializers/register-google-routes.js +17 -0
  61. package/addon/instance-initializers/register-osrm.js +5 -0
  62. package/addon/routes/application.js +6 -0
  63. package/addon/routes/operations/orchestrator.js +13 -0
  64. package/addon/routes/operations/orders/index/details.js +29 -22
  65. package/addon/routes/operations/orders/index/new.js +9 -0
  66. package/addon/routes/operations/orders/index.js +0 -1
  67. package/addon/routes/settings/map.js +3 -0
  68. package/addon/routes.js +1 -0
  69. package/addon/services/entity-actions.js +1 -1
  70. package/addon/services/geofence-event-bus.js +2 -1
  71. package/addon/services/geofence.js +204 -168
  72. package/addon/services/google-routes.js +300 -0
  73. package/addon/services/leaflet-draw-restriction.js +2 -1
  74. package/addon/services/leaflet-map-manager.js +17 -0
  75. package/addon/services/map-adapter/google.js +2205 -0
  76. package/addon/services/map-adapter/leaflet.js +1042 -0
  77. package/addon/services/map-adapter-interface.js +551 -0
  78. package/addon/services/map-manager.js +768 -0
  79. package/addon/services/map-settings.js +75 -0
  80. package/addon/services/movement-tracker.js +83 -32
  81. package/addon/services/order-actions.js +11 -1
  82. package/addon/services/osrm.js +79 -6
  83. package/addon/services/position-playback.js +135 -62
  84. package/addon/services/route-engine.js +83 -0
  85. package/addon/services/service-areas.js +8 -1
  86. package/addon/styles/fleetops-engine.css +555 -0
  87. package/addon/templates/management/vehicles/index.hbs +0 -1
  88. package/addon/templates/operations/orders/index/details.hbs +0 -1
  89. package/addon/templates/settings/map.hbs +53 -0
  90. package/addon/templates/settings/routing.hbs +15 -4
  91. package/addon/utils/fleet-ops-options.js +1 -0
  92. package/addon/utils/has-valid-coordinates.js +6 -0
  93. package/addon/utils/map-provider-options.js +6 -0
  94. package/addon/utils/order-route-editing.js +86 -0
  95. package/addon/utils/order-route-summary.js +20 -0
  96. package/addon/utils/place-address-html.js +41 -0
  97. package/addon/utils/route-colors.js +43 -12
  98. package/addon/utils/route-visualization.js +93 -0
  99. package/addon/utils/vendor-integration.js +2 -1
  100. package/app/components/admin/map-settings.js +1 -0
  101. package/app/components/admin/routing-settings.js +1 -0
  102. package/app/components/cell/order-route-type.js +1 -0
  103. package/app/components/cell/place-address.js +1 -0
  104. package/app/components/map/google-live-map.js +1 -0
  105. package/app/components/place/address.js +1 -0
  106. package/app/controllers/settings/map.js +1 -0
  107. package/app/helpers/place-address-html.js +1 -0
  108. package/app/routes/settings/map.js +1 -0
  109. package/app/services/map-adapter/google.js +1 -0
  110. package/app/services/map-adapter/leaflet.js +1 -0
  111. package/app/services/map-adapter-interface.js +1 -0
  112. package/app/services/map-manager.js +1 -0
  113. package/app/services/map-settings.js +1 -0
  114. package/app/templates/settings/map.js +1 -0
  115. package/app/utils/has-valid-coordinates.js +1 -0
  116. package/app/utils/order-route-editing.js +2 -0
  117. package/app/utils/order-route-summary.js +1 -0
  118. package/app/utils/place-address-html.js +1 -0
  119. package/app/utils/route-colors.js +1 -1
  120. package/app/utils/route-visualization.js +1 -0
  121. package/assets/images/drawing-controls-spritesheet.svg +156 -0
  122. package/composer.json +1 -1
  123. package/extension.json +1 -1
  124. package/package.json +4 -4
  125. package/pnpm-workspace.yaml +7 -0
  126. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  127. package/server/src/Http/Controllers/Api/v1/OrderController.php +31 -3
  128. package/server/src/Http/Controllers/Api/v1/PayloadController.php +14 -1
  129. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -0
  130. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +3 -2
  131. package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -0
  132. package/server/src/Http/Controllers/Internal/v1/LiveController.php +2 -1
  133. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +7 -4
  134. package/server/src/Http/Controllers/Internal/v1/OrderController.php +90 -34
  135. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +3 -2
  136. package/server/src/Http/Controllers/Internal/v1/SettingController.php +106 -3
  137. package/server/src/Http/Filter/OrderFilter.php +2 -1
  138. package/server/src/Http/Resources/v1/Index/Payload.php +9 -7
  139. package/server/src/Http/Resources/v1/Order.php +20 -1
  140. package/server/src/Models/Order.php +42 -1
  141. package/server/src/Models/OrderConfig.php +15 -0
  142. package/server/src/Models/Payload.php +131 -27
  143. package/server/src/Models/Place.php +182 -48
  144. package/server/src/Models/ServiceRate.php +4 -3
  145. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +59 -23
  146. package/server/src/Support/Utils.php +36 -2
  147. package/server/src/routes.php +4 -0
  148. 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,23 @@ 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({
117
+ street1: place.street1,
118
+ street2: place.street2,
119
+ city: place.city,
120
+ province: place.province,
121
+ postal_code: place.postal_code,
122
+ country: place.country,
123
+ location: place.location,
124
+ });
103
125
  this.previewRoute();
104
126
  }
105
127
 
@@ -129,13 +151,55 @@ export default class OrderFormRouteComponent extends Component {
129
151
  }
130
152
 
131
153
  @action async previewRoute() {
132
- if (!this.coordinates.length) return;
154
+ if (!this.coordinates.length) {
155
+ if (this.routingControl) {
156
+ this.mapManager.removeRoutingControl(this.routingControl);
157
+ this.routingControl = null;
158
+ }
159
+
160
+ return;
161
+ }
133
162
 
134
- const routingControl = await this.leafletMapManager.replaceRoutingControl(this.coordinates, this.routingControl, {
163
+ const order = this.args.resource;
164
+ const orderId = order.public_id ?? order.id ?? 'new-order';
165
+ const routeColor = colorForId(orderId);
166
+ const routeStatus = order.status ?? 'created';
167
+ const statusColor = routeColorForStatus(routeStatus);
168
+ const routeStyles = routeStyleForStatus(routeStatus, statusColor);
169
+ const isSinglePointPreview = this.coordinates.length === 1;
170
+ const fitOptions = isSinglePointPreview
171
+ ? {
172
+ paddingBottomRight: [0, 0],
173
+ panBy: ORDER_ROUTE_PREVIEW_SINGLE_POINT_PANBY,
174
+ }
175
+ : {
176
+ paddingBottomRight: ORDER_ROUTE_PREVIEW_PADDING_BOTTOM_RIGHT,
177
+ panBy: [0, 0],
178
+ maxZoom: this.coordinates.length === 2 ? ORDER_ROUTE_PREVIEW_MAX_ZOOM_TWO_POINTS : ORDER_ROUTE_PREVIEW_MAX_ZOOM_MULTI_POINTS,
179
+ };
180
+ const routingControl = await this.mapManager.replaceRoutingControl(this.coordinates, this.routingControl, {
181
+ engine: this.routeEngine.getDisplayEngine('osrm'),
182
+ color: statusColor,
183
+ orderId,
184
+ status: routeStatus,
185
+ fitOptions,
186
+ places: this.places,
187
+ markerWaypoints: this.coordinates,
188
+ polylineOptions: {
189
+ color: statusColor,
190
+ weight: routeStyles.at(-1)?.weight ?? 4,
191
+ opacity: routeStyles.at(-1)?.opacity ?? 0.85,
192
+ styles: routeStyles,
193
+ },
194
+ createMarker: (_waypoint, index) => {
195
+ const routePoint = this.routePoints[index];
196
+ return buildRoutePointMarkerPresentation(routePoint, routeColor);
197
+ },
135
198
  onRouteFound: (route) => this.setRoute(route),
136
199
  removeOptions: {
137
- filter: (layer) => layer.record_id === this.args.resource.driver_assigned?.id,
200
+ filter: (handle) => handle?.tag === this.args.resource.driver_assigned?.id,
138
201
  },
202
+ tag: this.args.resource.driver_assigned?.id,
139
203
  });
140
204
 
141
205
  this.routingControl = routingControl;
@@ -166,9 +230,10 @@ export default class OrderFormRouteComponent extends Component {
166
230
  const payload = order.payload;
167
231
  const waypoints = this.args.resource.payload.waypoints;
168
232
  const coordinates = this.coordinates.map((coord) => coord.reverse());
233
+ const service = this.routeEngine.getOptimizationEngine('osrm');
169
234
 
170
235
  try {
171
- const result = yield this.osrm.optimize({
236
+ const result = yield this.routeOptimization.optimize(service, {
172
237
  context: 'create_order',
173
238
  order,
174
239
  payload,
@@ -193,7 +258,10 @@ export default class OrderFormRouteComponent extends Component {
193
258
  }
194
259
 
195
260
  @action setOptimizedRoute(route, trip, waypoints, engine = 'osrm') {
196
- let summary = { totalDistance: trip.distance, totalTime: trip.duration };
261
+ let summary = {
262
+ totalDistance: trip?.distance ?? trip?.totalDistance ?? 0,
263
+ totalTime: trip?.duration ?? trip?.totalTime ?? 0,
264
+ };
197
265
  let payload = {
198
266
  optimized: true,
199
267
  coordinates: route,
@@ -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>