@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
@@ -2,75 +2,127 @@ import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { action } from '@ember/object';
4
4
  import { inject as service } from '@ember/service';
5
- import { isArray } from '@ember/array';
6
5
  import { task } from 'ember-concurrency';
7
- import { Point } from '@fleetbase/fleetops-data/utils/geojson';
6
+ import { buildRoutePointsFromPayload, describeRoutePoint } from '../../utils/route-visualization';
7
+ import {
8
+ applyOptimizedIntermediateWaypoints,
9
+ buildRouteOptimizationInput,
10
+ createWaypointRecord,
11
+ getPayloadIntermediateWaypoints,
12
+ getPayloadRouteCoordinates,
13
+ } from '../../utils/order-route-editing';
8
14
 
9
15
  export default class OrderRouteEditorComponent extends Component {
10
16
  @service store;
11
17
  @service intl;
12
18
  @service notifications;
13
19
  @service placeActions;
20
+ @service routeEngine;
14
21
  @service routeOptimization;
15
- @service osrm;
16
22
  @tracked route;
17
23
 
18
- get coordinates() {
19
- const payload = this.args.resource.payload;
20
- const waypoints = payload ? [payload.pickup, ...this.args.resource.payload.waypoints.map((waypoint) => waypoint.place), payload.dropoff] : [];
24
+ get payload() {
25
+ return this.args.resource.payload;
26
+ }
21
27
 
22
- return waypoints.filter((wp) => wp && wp.hasValidCoordinates).map((wp) => [wp.latitude, wp.longitude]);
28
+ get routePoints() {
29
+ return buildRoutePointsFromPayload(this.payload);
23
30
  }
24
31
 
25
- @action toggleMultiDropOrder(isMultiDrop) {
26
- const { pickup, dropoff } = this.args.resource.payload;
32
+ get routeStops() {
33
+ return this.routePoints.map((routePoint) => ({
34
+ routePoint,
35
+ place: routePoint.place,
36
+ badgeStyle: this.badgeStyleForRoutePoint(routePoint),
37
+ ...describeRoutePoint(routePoint, '#8B5CF6'),
38
+ }));
39
+ }
27
40
 
28
- if (isMultiDrop) {
29
- // if pickup move it to multipdrop
30
- if (pickup) {
31
- this.#addWaypointFromExistingPlace(pickup);
32
- }
41
+ get routeSummary() {
42
+ const segments = [];
33
43
 
34
- // if pickup move it to multipdrop
35
- if (dropoff) {
36
- this.#addWaypointFromExistingPlace(dropoff);
37
- }
44
+ if (this.payload?.pickup) {
45
+ segments.push('Pickup');
46
+ }
38
47
 
39
- this.args.resource.payload.setProperties({
40
- pickup: null,
41
- dropoff: null,
42
- return: null,
43
- pickup_uuid: null,
44
- dropoff_uuid: null,
45
- return_uuid: null,
46
- });
47
- } else {
48
- // get pickup from payload waypoints if available
49
- const waypoints =
50
- typeof this.args.resource.payload.waypoints.toArray === 'function' ? this.args.resource.payload.waypoints.toArray() : Array.from(this.args.resource.payload.waypoints);
51
-
52
- if (waypoints[0]) {
53
- const pickup = this.#createPlaceFromWaypoint(waypoints[0]);
54
- this.args.resource.payload.set('pickup', pickup);
55
- }
56
-
57
- if (waypoints[1]) {
58
- const dropoff = this.#createPlaceFromWaypoint(waypoints[1]);
59
- this.args.resource.payload.set('dropoff', dropoff);
60
- }
61
-
62
- this.args.resource.payload.setProperties({
63
- waypoints: [],
64
- });
48
+ if (this.intermediateWaypoints.length) {
49
+ segments.push(`${this.intermediateWaypoints.length} ${this.intermediateWaypoints.length === 1 ? 'Stop' : 'Stops'}`);
65
50
  }
66
51
 
67
- if (typeof this.args.onRouteChanged === 'function') {
68
- this.args.onRouteChanged();
52
+ if (this.payload?.dropoff) {
53
+ segments.push('Dropoff');
54
+ }
55
+
56
+ if (this.payload?.return) {
57
+ segments.push('Return');
69
58
  }
59
+
60
+ return segments.join(' • ');
61
+ }
62
+
63
+ get pickupStop() {
64
+ return this.routeStops.find((stop) => stop.routePoint?.role === 'pickup') ?? this.fallbackStop('pickup');
65
+ }
66
+
67
+ get dropoffStop() {
68
+ return this.routeStops.find((stop) => stop.routePoint?.role === 'dropoff') ?? this.fallbackStop('dropoff');
69
+ }
70
+
71
+ get returnStop() {
72
+ return {
73
+ label: 'R',
74
+ title: 'Return',
75
+ badgeStyle: this.badgeStyleForColor('#6B7280'),
76
+ };
77
+ }
78
+
79
+ get stopsSectionBadgeStyle() {
80
+ return this.badgeStyleForColor('#8B5CF6');
81
+ }
82
+
83
+ get intermediateRouteStops() {
84
+ return this.routeStops.filter((stop) => stop.routePoint?.role === 'waypoint');
85
+ }
86
+
87
+ get coordinates() {
88
+ return getPayloadRouteCoordinates(this.payload);
89
+ }
90
+
91
+ get intermediateWaypoints() {
92
+ return getPayloadIntermediateWaypoints(this.payload);
93
+ }
94
+
95
+ get canOptimizeRoute() {
96
+ return this.intermediateWaypoints.length >= 2;
97
+ }
98
+
99
+ badgeStyleForRoutePoint(routePoint) {
100
+ const { markerColor } = describeRoutePoint(routePoint, '#8B5CF6');
101
+ return this.badgeStyleForColor(markerColor);
102
+ }
103
+
104
+ badgeStyleForColor(color) {
105
+ const normalizedColor = color?.toLowerCase?.();
106
+ const isYellow = normalizedColor === '#facc15' || normalizedColor === '#ca8a04';
107
+ const textColor = isYellow ? '#111827' : '#ffffff';
108
+
109
+ return `background-color: ${color}; color: ${textColor};`;
110
+ }
111
+
112
+ fallbackStop(role) {
113
+ const routePoint = {
114
+ role,
115
+ stopNumber: role === 'pickup' ? null : role === 'dropoff' ? null : 1,
116
+ };
117
+
118
+ return {
119
+ badgeStyle: this.badgeStyleForRoutePoint(routePoint),
120
+ ...describeRoutePoint(routePoint, '#8B5CF6'),
121
+ };
70
122
  }
71
123
 
72
124
  @action removeWaypoint(waypoint) {
73
- this.args.resource.payload.waypoints.removeObject(waypoint);
125
+ this.payload.waypoints.removeObject(waypoint);
74
126
 
75
127
  if (typeof this.args.onWaypointRemoved === 'function') {
76
128
  this.args.onWaypointRemoved(waypoint);
@@ -82,23 +134,29 @@ export default class OrderRouteEditorComponent extends Component {
82
134
  }
83
135
 
84
136
  @action addWaypoint() {
85
- const location = new Point(0, 0);
86
- const place = this.store.createRecord('place', { location });
87
- const waypoint = this.store.createRecord('waypoint', { place, location });
88
-
89
- this.args.resource.payload.waypoints.pushObject(waypoint);
137
+ const waypoint = createWaypointRecord(this.store);
138
+ this.payload.waypoints.pushObject(waypoint);
90
139
 
91
140
  if (typeof this.args.onWaypointAdded === 'function') {
92
141
  this.args.onWaypointAdded(waypoint);
93
142
  }
143
+
144
+ if (typeof this.args.onRouteChanged === 'function') {
145
+ this.args.onRouteChanged();
146
+ }
94
147
  }
95
148
 
96
149
  @action setWaypointPlace(index, place) {
97
- if (isArray(this.args.resource.payload.waypoints) && !this.args.resource.payload.waypoints.objectAt(index)) return;
150
+ const waypoints = this.intermediateWaypoints;
151
+ const waypoint = waypoints[index];
98
152
 
99
- const json = place.serialize();
100
- this.args.resource.payload.waypoints.objectAt(index).setProperties({
101
- uuid: place.id,
153
+ if (!waypoint) {
154
+ return;
155
+ }
156
+
157
+ const json = typeof place?.serialize === 'function' ? place.serialize() : {};
158
+ waypoint.setProperties({
159
+ uuid: waypoint.uuid ?? place.id,
102
160
  place_uuid: place.id,
103
161
  location: place.location,
104
162
  place,
@@ -115,7 +173,7 @@ export default class OrderRouteEditorComponent extends Component {
115
173
  }
116
174
 
117
175
  @action setPayloadPlace(prop, place) {
118
- this.args.resource.payload.set(prop, place);
176
+ this.payload.set(prop, place);
119
177
 
120
178
  if (typeof this.args.onPlaceSelected === 'function') {
121
179
  this.args.onPlaceSelected(place);
@@ -126,6 +184,14 @@ export default class OrderRouteEditorComponent extends Component {
126
184
  }
127
185
  }
128
186
 
187
+ @action clearPayloadPlace(prop) {
188
+ this.payload.set(prop, null);
189
+
190
+ if (typeof this.args.onRouteChanged === 'function') {
191
+ this.args.onRouteChanged();
192
+ }
193
+ }
194
+
129
195
  @action sortWaypoints({ sourceList, sourceIndex, targetList, targetIndex }) {
130
196
  if (sourceList === targetList && sourceIndex === targetIndex) return;
131
197
 
@@ -140,18 +206,12 @@ export default class OrderRouteEditorComponent extends Component {
140
206
  }
141
207
 
142
208
  @task *optimizeRouteWithService(service) {
143
- const order = this.args.resource;
144
- const payload = order.payload;
145
- const waypoints = this.args.resource.payload.waypoints;
146
- const coordinates = this.coordinates.map((coord) => coord.reverse());
209
+ const optimizationInput = buildRouteOptimizationInput(this.args.resource);
147
210
 
148
211
  try {
149
212
  const result = yield this.routeOptimization.optimize(service, {
150
213
  context: 'edit_order_route',
151
- order,
152
- payload,
153
- waypoints,
154
- coordinates,
214
+ ...optimizationInput,
155
215
  });
156
216
  this.handleRouteOptimization(result);
157
217
  } catch (err) {
@@ -160,39 +220,41 @@ export default class OrderRouteEditorComponent extends Component {
160
220
  }
161
221
 
162
222
  @task *optimizeRoute() {
163
- const order = this.args.resource;
164
- const payload = order.payload;
165
- const waypoints = this.args.resource.payload.waypoints;
166
- const coordinates = this.coordinates.map((coord) => coord.reverse());
223
+ const optimizationInput = buildRouteOptimizationInput(this.args.resource);
224
+ const service = this.routeEngine.getOptimizationEngine('osrm');
167
225
 
168
226
  try {
169
- const result = yield this.osrm.optimize({
227
+ const result = yield this.routeOptimization.optimize(service, {
170
228
  context: 'edit_order_route',
171
- order,
172
- payload,
173
- waypoints,
174
- coordinates,
229
+ ...optimizationInput,
175
230
  });
176
231
 
177
232
  this.handleRouteOptimization(result);
178
- } catch (err) {
233
+ } catch (_err) {
179
234
  this.notifications.error(this.intl.t('fleet-ops.operations.orders.index.new.route-error'));
180
235
  }
181
236
  }
182
237
 
183
238
  @action handleRouteOptimization({ sortedWaypoints, route, trip, result, engine = 'osrm' }) {
184
- // Update controller state
185
- this.args.resource.payload.waypoints = sortedWaypoints;
239
+ applyOptimizedIntermediateWaypoints(this.payload, sortedWaypoints);
240
+
186
241
  if (route) {
187
- this.setOptimizedRoute(route, trip, result.waypoints, engine);
242
+ this.setOptimizedRoute(route, trip, result?.waypoints, engine);
188
243
  }
189
- // this.previewRoute();
244
+
190
245
  this.args.resource.set('optimized', true);
246
+
247
+ if (typeof this.args.onRouteChanged === 'function') {
248
+ this.args.onRouteChanged();
249
+ }
191
250
  }
192
251
 
193
252
  @action setOptimizedRoute(route, trip, waypoints, engine = 'osrm') {
194
- let summary = { totalDistance: trip.distance, totalTime: trip.duration };
195
- let payload = {
253
+ const summary = {
254
+ totalDistance: trip?.distance ?? trip?.totalDistance ?? 0,
255
+ totalTime: trip?.duration ?? trip?.totalTime ?? 0,
256
+ };
257
+ const payload = {
196
258
  optimized: true,
197
259
  coordinates: route,
198
260
  waypoints,
@@ -209,25 +271,4 @@ export default class OrderRouteEditorComponent extends Component {
209
271
  this.args.resource.route = routeModel;
210
272
  this.route = payload;
211
273
  }
212
-
213
- #createPlaceFromWaypoint(waypoint) {
214
- const json = waypoint.serialize();
215
- return this.store.createRecord('place', json);
216
- }
217
-
218
- #addWaypointFromExistingPlace(place) {
219
- const json = place.serialize();
220
- const waypoint = this.store.createRecord('waypoint', {
221
- uuid: place.id,
222
- place_uuid: place.id,
223
- location: place.location,
224
- place,
225
- ...json,
226
- });
227
- this.args.resource.payload.waypoints.pushObject(waypoint);
228
-
229
- if (typeof this.args.onWaypointAdded === 'function') {
230
- this.args.onWaypointAdded(waypoint);
231
- }
232
- }
233
274
  }
@@ -0,0 +1 @@
1
+ {{this.addressHtml}}
@@ -0,0 +1,12 @@
1
+ import Component from '@glimmer/component';
2
+ import placeAddressHtml from '../../utils/place-address-html';
3
+
4
+ export default class PlaceAddressComponent extends Component {
5
+ get place() {
6
+ return this.args.resource ?? this.args.place;
7
+ }
8
+
9
+ get addressHtml() {
10
+ return placeAddressHtml(this.place);
11
+ }
12
+ }
@@ -1,70 +1,58 @@
1
1
  <div class="order-route-list" ...attributes>
2
- {{#if @order.isMultipleDropoffOrder}}
3
- {{#if @order.payload.firstWaypoint}}
4
- <div class="order-route-stop {{@routeStopClass}}">
5
- <div class="order-route-stop-index {{@routeStopIndexClass}}">
6
- <div class="index-count">1</div>
7
- </div>
8
- <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
9
- <DisplayPlace @place={{@order.payload.firstWaypoint}} @eta={{get @eta @order.payload.firstWaypoint.id}} @waypointActions={{@waypointActions}} />
10
- </div>
2
+ {{#if this.firstStop}}
3
+ <div class="order-route-stop {{@routeStopClass}}">
4
+ <div class="order-route-stop-index {{@routeStopIndexClass}}">
5
+ <div class="index-count fleetops-route-stop-badge" style={{this.firstStop.badgeStyle}}>{{this.firstStop.label}}</div>
6
+ </div>
7
+ <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
8
+ <DisplayPlace @place={{this.firstStop.place}} @eta={{get @eta this.firstStop.place.id}} @waypointActions={{@waypointActions}} />
11
9
  </div>
12
- {{/if}}
10
+ </div>
11
+ {{/if}}
13
12
 
14
- {{#if (gt @order.payload.waypoints.length 2)}}
15
- <Button class="btn-reset w-full" @wrapperClass="w-full" @onClick={{this.toggleWaypointsCollapse}}>
16
- <div class="px-3 py-2 w-full bg-yellow-200 text-yellow-900 rounded shadow-sm flex flex-col justify-center items-start mb-3">
17
- <div class="flex flex-row items-center">
18
- <FaIcon @icon="eye" class="mr-2 text-yellow-900" />
19
- <span class="font-bold">{{if this.isWaypointsCollapsed (t "route-list.expand") (t "route-list.collapse")}}</span>
13
+ {{#if this.shouldCollapseWaypoints}}
14
+ <Button class="btn-reset w-full" @wrapperClass="w-full" @onClick={{this.toggleWaypointsCollapse}}>
15
+ <div class="px-3 py-2 w-full bg-yellow-200 text-yellow-900 rounded shadow-sm flex flex-col justify-center items-start mb-3">
16
+ <div class="flex flex-row items-center">
17
+ <FaIcon @icon="eye" class="mr-2 text-yellow-900" />
18
+ <span class="font-bold">{{if this.isWaypointsCollapsed (t "route-list.expand") (t "route-list.collapse")}}</span>
19
+ </div>
20
+ <div>{{this.middleStops.length}} {{t "route-list.more-waypoints"}}</div>
21
+ </div>
22
+ </Button>
23
+ {{#unless this.isWaypointsCollapsed}}
24
+ {{#each this.middleStops as |stop|}}
25
+ <div class="order-route-stop {{@routeStopClass}}">
26
+ <div class="order-route-stop-index {{@routeStopIndexClass}}">
27
+ <div class="index-count fleetops-route-stop-badge" style={{stop.badgeStyle}}>{{stop.label}}</div>
20
28
  </div>
21
- <div>{{@order.payload.middleWaypoints.length}} {{t "route-list.more-waypoints"}}</div>
22
- </div>
23
- </Button>
24
- {{#unless this.isWaypointsCollapsed}}
25
- {{#each @order.payload.middleWaypoints as |waypoint index|}}
26
- <div class="order-route-stop {{@routeStopClass}}">
27
- <div class="order-route-stop-index {{@routeStopIndexClass}}">
28
- <div class="index-count">{{add index 2}}</div>
29
- </div>
30
- <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
31
- <DisplayPlace @place={{waypoint}} @eta={{get @eta waypoint.id}} @waypointActions={{@waypointActions}} />
32
- </div>
29
+ <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
30
+ <DisplayPlace @place={{stop.place}} @eta={{get @eta stop.place.id}} @waypointActions={{@waypointActions}} />
33
31
  </div>
34
- {{/each}}
35
- {{/unless}}
36
- {{/if}}
37
-
38
- {{#if @order.payload.lastWaypoint}}
39
- <div class="order-route-stop {{@routeStopClass}}">
40
- <div class="order-route-stop-index {{@routeStopIndexClass}}">
41
- <div class="index-count">{{add @order.payload.middleWaypoints.length 2}}</div>
42
32
  </div>
43
- <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
44
- <DisplayPlace @place={{@order.payload.lastWaypoint}} @eta={{get @eta @order.payload.lastWaypoint.id}} @waypointActions={{@waypointActions}} />
45
- </div>
46
- </div>
47
- {{/if}}
48
- {{else}}
49
- {{#if @order.payload.pickup}}
33
+ {{/each}}
34
+ {{/unless}}
35
+ {{else if this.hasExtraStops}}
36
+ {{#each this.middleStops as |stop|}}
50
37
  <div class="order-route-stop {{@routeStopClass}}">
51
38
  <div class="order-route-stop-index {{@routeStopIndexClass}}">
52
- <div class="index-count">1</div>
39
+ <div class="index-count fleetops-route-stop-badge" style={{stop.badgeStyle}}>{{stop.label}}</div>
53
40
  </div>
54
41
  <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
55
- <DisplayPlace @place={{@order.payload.pickup}} @eta={{get @eta @order.payload.pickup.id}} />
42
+ <DisplayPlace @place={{stop.place}} @eta={{get @eta stop.place.id}} @waypointActions={{@waypointActions}} />
56
43
  </div>
57
44
  </div>
58
- {{/if}}
59
- {{#if @order.payload.dropoff}}
60
- <div class="order-route-stop {{@routeStopClass}}">
61
- <div class="order-route-stop-index {{@routeStopIndexClass}}">
62
- <div class="index-count">2</div>
63
- </div>
64
- <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
65
- <DisplayPlace @place={{@order.payload.dropoff}} @eta={{get @eta @order.payload.dropoff.id}} />
66
- </div>
45
+ {{/each}}
46
+ {{/if}}
47
+
48
+ {{#if this.lastStop}}
49
+ <div class="order-route-stop {{@routeStopClass}}">
50
+ <div class="order-route-stop-index {{@routeStopIndexClass}}">
51
+ <div class="index-count fleetops-route-stop-badge" style={{this.lastStop.badgeStyle}}>{{this.lastStop.label}}</div>
52
+ </div>
53
+ <div class="order-route-location {{@routeLocationClass}} dark:text-gray-100">
54
+ <DisplayPlace @place={{this.lastStop.place}} @eta={{get @eta this.lastStop.place.id}} @waypointActions={{@waypointActions}} />
67
55
  </div>
68
- {{/if}}
56
+ </div>
69
57
  {{/if}}
70
58
  </div>
@@ -1,10 +1,57 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { action } from '@ember/object';
4
+ import { buildRoutePointsFromPayload, describeRoutePoint } from '../utils/route-visualization';
5
+ import { colorForId } from '../utils/route-colors';
4
6
 
5
7
  export default class RouteListComponent extends Component {
6
8
  @tracked isWaypointsCollapsed = true;
7
9
 
10
+ get routeColor() {
11
+ const order = this.args.order;
12
+
13
+ return colorForId(order?.public_id ?? order?.id ?? 'order-route');
14
+ }
15
+
16
+ get routeStops() {
17
+ const routePoints = buildRoutePointsFromPayload(this.args.order?.payload);
18
+
19
+ return routePoints.map((routePoint) => ({
20
+ routePoint,
21
+ place: routePoint.place,
22
+ badgeStyle: this.badgeStyleForStop(routePoint),
23
+ ...describeRoutePoint(routePoint, this.routeColor),
24
+ }));
25
+ }
26
+
27
+ get firstStop() {
28
+ return this.routeStops[0] ?? null;
29
+ }
30
+
31
+ get middleStops() {
32
+ return this.routeStops.slice(1, -1);
33
+ }
34
+
35
+ get lastStop() {
36
+ return this.routeStops.length > 1 ? this.routeStops.at(-1) : null;
37
+ }
38
+
39
+ get hasExtraStops() {
40
+ return this.routeStops.length > 2;
41
+ }
42
+
43
+ get shouldCollapseWaypoints() {
44
+ return this.args.isCollapsible !== false && this.hasExtraStops;
45
+ }
46
+
47
+ badgeStyleForStop(routePoint) {
48
+ const { markerColor } = describeRoutePoint(routePoint, this.routeColor);
49
+ const isYellow = markerColor?.toLowerCase?.() === '#facc15' || markerColor?.toLowerCase?.() === '#ca8a04';
50
+ const textColor = isYellow ? '#111827' : '#ffffff';
51
+
52
+ return `background-color: ${markerColor}; color: ${textColor};`;
53
+ }
54
+
8
55
  @action toggleWaypointsCollapse() {
9
56
  this.isWaypointsCollapsed = !this.isWaypointsCollapsed;
10
57
  }
@@ -6,7 +6,7 @@ import contextComponentCallback from '@fleetbase/ember-core/utils/context-compon
6
6
  import applyContextComponentArguments from '@fleetbase/ember-core/utils/apply-context-component-arguments';
7
7
 
8
8
  export default class RouteOptimizationWizardPanelComponent extends Component {
9
- @service universe;
9
+ @service sidebar;
10
10
  @tracked context;
11
11
 
12
12
  /**
@@ -27,9 +27,8 @@ export default class RouteOptimizationWizardPanelComponent extends Component {
27
27
  @action setOverlayContext(overlayContext) {
28
28
  this.context = overlayContext;
29
29
  // hide sidebar
30
- if (this.universe.sidebarContext) {
31
- this.universe.sidebarContext.hide();
32
- }
30
+ this.sidebar.hide();
31
+
33
32
  contextComponentCallback(this, 'onLoad', ...arguments);
34
33
  }
35
34
 
@@ -1,7 +1,7 @@
1
1
  <Layout::Resource::Card ...attributes as |Card|>
2
- <Card.header class="{{@headerClass}} truncate">
3
- <div class="font-semibold">{{or @resource.name @resource.yearMakeModel}}</div>
4
- <div class="text-gray-300 dark:text-gray-500 text-sm">{{or @resource.plate_number @resource.vin @resource.serial_number @resource.call_sign @resource.public_id}}</div>
2
+ <Card.header class="{{@headerClass}}">
3
+ <div class="font-semibold w-full truncate">{{or @resource.name @resource.yearMakeModel}}</div>
4
+ <div class="text-gray-300 dark:text-gray-500 text-sm w-full truncate">{{or @resource.plate_number @resource.vin @resource.serial_number @resource.call_sign @resource.public_id}}</div>
5
5
  {{#if (has-block "header")}}
6
6
  {{yield to="header"}}
7
7
  {{/if}}
@@ -21,7 +21,7 @@
21
21
  {{#if (has-block "footer")}}
22
22
  {{yield to="footer"}}
23
23
  {{/if}}
24
- <div class="mt-1">
24
+ <div class="mt-1.5">
25
25
  <div class="text-gray-300 dark:text-gray-500 text-xs">Last Modified: {{@resource.updatedAt}}</div>
26
26
  </div>
27
27
  </Card.footer>
@@ -1,4 +1,5 @@
1
1
  import Controller from '@ember/controller';
2
+ import { isArray } from '@ember/array';
2
3
  import { inject as service } from '@ember/service';
3
4
  import { tracked } from '@glimmer/tracking';
4
5
  import { action } from '@ember/object';
@@ -75,7 +76,7 @@ export default class MaintenanceSchedulesIndexController extends Controller {
75
76
  })
76
77
  .then((response) => {
77
78
  // The API returns { events: [...] } — unwrap the array.
78
- const raw = Array.isArray(response) ? response : (response?.events ?? []);
79
+ const raw = isArray(response) ? response : (response?.events ?? []);
79
80
  const events = raw.map((event) => ({
80
81
  id: event.id,
81
82
  title: event.title,