@fleetbase/fleetops-engine 0.4.3 → 0.4.4

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 (30) hide show
  1. package/addon/components/edit-order-route-panel.hbs +182 -0
  2. package/addon/components/edit-order-route-panel.js +265 -0
  3. package/addon/components/fleet-panel.js +3 -3
  4. package/addon/components/map-container/toolbar.hbs +2 -2
  5. package/addon/components/modals/order-route-form.hbs +1 -1
  6. package/addon/controllers/operations/orders/index/new.js +66 -22
  7. package/addon/controllers/operations/orders/index/view.js +131 -126
  8. package/addon/routes/operations/orders/index/view.js +1 -1
  9. package/addon/services/context-panel.js +6 -0
  10. package/addon/styles/fleetops-engine.css +5 -0
  11. package/addon/templates/operations/orders/index/new.hbs +390 -58
  12. package/addon/templates/operations/orders/index/view.hbs +135 -105
  13. package/addon/templates/operations/service-rates/index/new.hbs +3 -1
  14. package/composer.json +2 -2
  15. package/extension.json +1 -1
  16. package/package.json +3 -3
  17. package/server/src/Http/Controllers/Api/v1/DriverController.php +59 -20
  18. package/server/src/Http/Controllers/Api/v1/OrderController.php +2 -0
  19. package/server/src/Http/Controllers/Internal/v1/DriverController.php +16 -3
  20. package/server/src/Http/Controllers/Internal/v1/OrderController.php +11 -0
  21. package/server/src/Http/Resources/v1/Order.php +3 -0
  22. package/server/src/Http/Resources/v1/PurchaseRate.php +2 -1
  23. package/server/src/Http/Resources/v1/ServiceQuote.php +1 -0
  24. package/server/src/Http/Resources/v1/ServiceQuoteItem.php +29 -0
  25. package/server/src/Models/Driver.php +1 -1
  26. package/server/src/Models/Order.php +16 -0
  27. package/server/src/Models/Place.php +2 -2
  28. package/server/src/Observers/DriverObserver.php +6 -1
  29. package/server/src/routes.php +1 -0
  30. package/translations/en-us.yaml +9 -2
@@ -3,13 +3,11 @@ import { inject as controller } from '@ember/controller';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { tracked } from '@glimmer/tracking';
5
5
  import { action, computed } from '@ember/object';
6
- import { isArray } from '@ember/array';
7
6
  import { later } from '@ember/runloop';
8
7
  import { not, notEmpty, alias } from '@ember/object/computed';
9
8
  import { task } from 'ember-concurrency-decorators';
10
9
  import { OSRMv1, Control as RoutingControl } from '@fleetbase/leaflet-routing-machine';
11
10
  import groupBy from '@fleetbase/ember-core/utils/macros/group-by';
12
- import findClosestWaypoint from '@fleetbase/ember-core/utils/find-closest-waypoint';
13
11
  import getRoutingHost from '@fleetbase/ember-core/utils/get-routing-host';
14
12
 
15
13
  export default class OperationsOrdersIndexViewController extends BaseController {
@@ -115,6 +113,29 @@ export default class OperationsOrdersIndexViewController extends BaseController
115
113
  @tracked isWaypointsCollapsed;
116
114
  @tracked leafletRoute;
117
115
  @tracked routeControl;
116
+ @tracked commentInput = '';
117
+ @tracked uploadQueue = [];
118
+ acceptedFileTypes = [
119
+ 'application/vnd.ms-excel',
120
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
121
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
122
+ 'application/msword',
123
+ 'application/pdf',
124
+ 'application/x-pdf',
125
+ 'image/jpeg',
126
+ 'image/png',
127
+ 'image/gif',
128
+ 'image/webp',
129
+ 'video/mp4',
130
+ 'video/quicktime',
131
+ 'video/x-msvideo',
132
+ 'video/x-flv',
133
+ 'video/x-ms-wmv',
134
+ 'audio/mpeg',
135
+ 'video/x-msvideo',
136
+ 'application/zip',
137
+ 'application/x-tar',
138
+ ];
118
139
 
119
140
  @alias('currentUser.latitude') userLatitude;
120
141
  @alias('currentUser.longitude') userLongitude;
@@ -133,16 +154,16 @@ export default class OperationsOrdersIndexViewController extends BaseController
133
154
  ];
134
155
 
135
156
  @tracked routePanelButtons = [
136
- {
137
- type: 'default',
138
- text: 'Edit',
139
- icon: 'pencil',
140
- iconPrefix: 'fas',
141
- onClick: () => {
142
- const order = this.model;
143
- this.editOrderRoute(order);
144
- },
145
- },
157
+ // {
158
+ // type: 'default',
159
+ // text: 'Edit',
160
+ // icon: 'pencil',
161
+ // iconPrefix: 'fas',
162
+ // onClick: () => {
163
+ // const order = this.model;
164
+ // this.editOrderRoute(order);
165
+ // },
166
+ // },
146
167
  ];
147
168
 
148
169
  @not('isWaypointsCollapsed') waypointsIsNotCollapsed;
@@ -179,6 +200,25 @@ export default class OperationsOrdersIndexViewController extends BaseController
179
200
  return groups;
180
201
  }
181
202
 
203
+ @computed('model.payload.{dropoff,pickup,waypoints}') get routeWaypoints() {
204
+ const { payload } = this.model;
205
+ let waypoints = [];
206
+ let coordinates = [];
207
+
208
+ waypoints.pushObjects([payload.pickup, ...payload.waypoints.toArray(), payload.dropoff]);
209
+ waypoints.forEach((place) => {
210
+ if (place && place.get('longitude') && place.get('latitude')) {
211
+ if (place.hasInvalidCoordinates) {
212
+ return;
213
+ }
214
+
215
+ coordinates.pushObject([place.get('latitude'), place.get('longitude')]);
216
+ }
217
+ });
218
+
219
+ return coordinates;
220
+ }
221
+
182
222
  @task *loadOrderRelations(order) {
183
223
  yield order.loadPayload();
184
224
  yield order.loadDriver();
@@ -186,6 +226,8 @@ export default class OperationsOrdersIndexViewController extends BaseController
186
226
  yield order.loadCustomer();
187
227
  yield order.loadTrackingActivity();
188
228
  yield order.loadOrderConfig();
229
+ yield order.loadPurchaseRate();
230
+ yield order.loadFiles();
189
231
  }
190
232
 
191
233
  @action resetView() {
@@ -280,8 +322,7 @@ export default class OperationsOrdersIndexViewController extends BaseController
280
322
  setup();
281
323
  }
282
324
 
283
- @computed('model.payload.{dropoff,pickup,waypoints}') get routeWaypoints() {
284
- const { payload } = this.model;
325
+ getPayloadCoordinates(payload) {
285
326
  let waypoints = [];
286
327
  let coordinates = [];
287
328
 
@@ -299,11 +340,19 @@ export default class OperationsOrdersIndexViewController extends BaseController
299
340
  return coordinates;
300
341
  }
301
342
 
343
+ getPayloadWaypointsAsArray() {
344
+ if (this.model.payload && this.model.payload.waypoints) {
345
+ return this.model.payload.waypoints.toArray();
346
+ }
347
+
348
+ return [];
349
+ }
350
+
302
351
  @action displayOrderRoute() {
303
- const payload = this.model.payload;
304
- const waypoints = this.routeWaypoints;
305
352
  const leafletMap = this.leafletMap;
306
- const routingHost = getRoutingHost(payload, payload?.waypoints?.toArray());
353
+ const payload = this.model.payload;
354
+ const waypoints = this.getPayloadCoordinates(payload);
355
+ const routingHost = getRoutingHost(payload, this.getPayloadWaypointsAsArray());
307
356
 
308
357
  if (!waypoints || waypoints.length < 2 || !leafletMap) {
309
358
  return;
@@ -334,7 +383,7 @@ export default class OperationsOrdersIndexViewController extends BaseController
334
383
  router,
335
384
  }).addTo(leafletMap);
336
385
 
337
- this.routeControl?.on('routesfound', (event) => {
386
+ this.routeControl.on('routesfound', (event) => {
338
387
  const { routes } = event;
339
388
 
340
389
  this.leafletRoute = routes.firstObject;
@@ -343,7 +392,7 @@ export default class OperationsOrdersIndexViewController extends BaseController
343
392
  later(
344
393
  this,
345
394
  () => {
346
- leafletMap?.flyToBounds(waypoints, {
395
+ leafletMap.flyToBounds(waypoints, {
347
396
  paddingBottomRight: [200, 0],
348
397
  maxZoom: 14,
349
398
  animate: true,
@@ -412,9 +461,14 @@ export default class OperationsOrdersIndexViewController extends BaseController
412
461
  order,
413
462
  confirm: (modal) => {
414
463
  modal.startLoading();
415
- return order.save().then(() => {
416
- this.notifications.success(options.successNotification || this.intl.t('fleet-ops.operations.orders.index.view.update-success', { orderId: order.public_id }));
417
- });
464
+ return order
465
+ .save()
466
+ .then(() => {
467
+ this.notifications.success(options.successNotification || this.intl.t('fleet-ops.operations.orders.index.view.update-success', { orderId: order.public_id }));
468
+ })
469
+ .catch((error) => {
470
+ this.notifications.serverError(error);
471
+ });
418
472
  },
419
473
  decline: () => {
420
474
  order.payload.rollbackAttributes();
@@ -475,114 +529,30 @@ export default class OperationsOrdersIndexViewController extends BaseController
475
529
  * @void
476
530
  */
477
531
  @action async editOrderRoute(order, options = {}) {
478
- options = options === null ? {} : options;
479
-
480
- await this.modalsManager.done();
481
-
482
- this.modalsManager.show('modals/order-route-form', {
483
- title: this.intl.t('fleet-ops.operations.orders.index.view.edit-route-title'),
484
- acceptButtonText: 'Save Changes',
485
- acceptButtonIcon: 'save',
486
- order,
487
- userLatitude: this.userLatitude,
488
- userLongitude: this.userLongitude,
489
- isOptimizingRoute: false,
490
- editPlace: async (place) => {
491
- await this.modalsManager.done();
492
-
493
- this.contextPanel.focus(place, 'editing', {
494
- args: {
495
- onClose: () => {
496
- this.editOrderRoute(order);
497
- },
498
- },
499
- });
500
- },
501
- toggleMultiDropOrder: () => {
502
- if (order.payload.isMultiDrop) {
503
- order.payload.waypoints.clear();
504
- order.payload.setProperties({
505
- pickup_uuid: '#',
506
- dropoff_uuid: '#',
507
- });
508
- } else {
509
- const waypoint = this.store.createRecord('waypoint');
510
- order.payload.waypoints.pushObject(waypoint);
511
-
512
- order.payload.setProperties({
513
- pickup_uuid: null,
514
- dropoff_uuid: null,
515
- pickup: null,
516
- dropoff: null,
517
- });
518
- }
519
- },
520
- removeWaypoint: (waypoint) => {
521
- order.payload.waypoints.removeObject(waypoint);
522
- },
523
- addWaypoint: () => {
524
- const waypoint = this.store.createRecord('waypoint');
525
- order.payload.waypoints.pushObject(waypoint);
526
- },
527
- setWaypointPlace: (index, place) => {
528
- if (!order.payload.waypoints?.objectAt(index)) {
529
- return;
530
- }
531
-
532
- order.payload.waypoints.objectAt(index).setProperties({
533
- name: place.name,
534
- place_uuid: place.id,
535
- place,
536
- });
537
- },
538
- setPayloadPlace: (prop, place) => {
539
- order.payload.set(prop, place);
540
- },
541
- optimizeRoute: async () => {
542
- this.modalsManager.setOption('isOptimizingRoute', true);
543
-
544
- const coordinates = order.payload.payloadCoordinates;
545
- const routingHost = getRoutingHost(order.payload, order.payload.waypoints);
546
-
547
- const response = await this.fetch.routing(coordinates, { source: 'any', destination: 'any', annotations: true }, { host: routingHost }).catch(() => {
548
- this.notifications.error(this.intl.t('fleet-ops.operations.orders.index.view.route-error'));
549
- this.modalsManager.setOption('isOptimizingRoute', false);
550
- });
551
-
552
- if (response && response.code === 'Ok') {
553
- if (response.waypoints && isArray(response.waypoints)) {
554
- const responseWaypoints = response.waypoints.sortBy('waypoint_index');
555
- const sortedWaypoints = [];
556
-
557
- for (let i = 0; i < responseWaypoints.length; i++) {
558
- const optimizedWaypoint = responseWaypoints.objectAt(i);
559
- const optimizedWaypointLongitude = optimizedWaypoint.location.firstObject;
560
- const optimizedWaypointLatitude = optimizedWaypoint.location.lastObject;
561
- const waypointModel = findClosestWaypoint(optimizedWaypointLatitude, optimizedWaypointLongitude, order.payload.waypoints);
562
-
563
- sortedWaypoints.pushObject(waypointModel);
564
- }
565
-
566
- order.payload.waypoints = sortedWaypoints;
567
- }
568
- } else {
569
- this.notifications.error(this.intl.t('fleet-ops.operations.orders.index.view.route-error'));
570
- }
532
+ const updateRouteDisplay = () => {
533
+ later(
534
+ this,
535
+ () => {
536
+ this.displayOrderRoute();
537
+ },
538
+ 100
539
+ );
540
+ };
571
541
 
572
- this.modalsManager.setOption('isOptimizingRoute', false);
542
+ this.contextPanel.focus(order, 'editingRoute', {
543
+ args: {
544
+ isResizable: true,
545
+ latitude: this.userLatitude,
546
+ longitude: this.userLongitude,
547
+ ...options,
573
548
  },
574
- confirm: (modal) => {
575
- modal.startLoading();
576
-
577
- return order.payload.save().then(() => {
578
- this.notifications.success(options.successNotification ?? this.intl.t('fleet-ops.operations.orders.index.view.route-update-success', { orderId: order.public_id }));
579
- });
549
+ onRouteChanged: () => {
550
+ updateRouteDisplay();
580
551
  },
581
- decline: () => {
582
- order.payload.rollbackAttributes();
583
- this.modalsManager.done();
552
+ onAfterSave: () => {
553
+ this.contextPanel.clear();
554
+ updateRouteDisplay();
584
555
  },
585
- ...options,
586
556
  });
587
557
  }
588
558
 
@@ -911,4 +881,39 @@ export default class OperationsOrdersIndexViewController extends BaseController
911
881
  },
912
882
  });
913
883
  }
884
+
885
+ @action queueFile(file) {
886
+ // since we have dropzone and upload button within dropzone validate the file state first
887
+ // as this method can be called twice from both functions
888
+ if (['queued', 'failed', 'timed_out', 'aborted'].indexOf(file.state) === -1) {
889
+ return;
890
+ }
891
+
892
+ // Queue and upload immediatley
893
+ this.uploadQueue.pushObject(file);
894
+ this.fetch.uploadFile.perform(
895
+ file,
896
+ {
897
+ path: 'uploads/fleet-ops/order-files',
898
+ subject_uuid: this.model.id,
899
+ subject_type: 'fleet-ops:order',
900
+ type: 'order_file',
901
+ },
902
+ (uploadedFile) => {
903
+ this.model.files.pushObject(uploadedFile);
904
+ this.uploadQueue.removeObject(file);
905
+ },
906
+ () => {
907
+ this.uploadQueue.removeObject(file);
908
+ // remove file from queue
909
+ if (file.queue && typeof file.queue.remove === 'function') {
910
+ file.queue.remove(file);
911
+ }
912
+ }
913
+ );
914
+ }
915
+
916
+ @action removeFile(file) {
917
+ return file.destroyRecord();
918
+ }
914
919
  }
@@ -29,7 +29,7 @@ export default class OperationsOrdersIndexViewRoute extends Route {
29
29
  return this.store.queryRecord('order', {
30
30
  public_id,
31
31
  single: true,
32
- with: ['payload', 'driverAssigned', 'customer', 'facilitator', 'trackingStatuses', 'trackingNumber'],
32
+ with: ['payload', 'driverAssigned', 'customer', 'facilitator', 'trackingStatuses', 'trackingNumber', 'purchaseRate', 'comments', 'files'],
33
33
  });
34
34
  }
35
35
 
@@ -97,6 +97,12 @@ export default class ContextPanelService extends Service {
97
97
  componentArguments: [{ isResizable: true }, { width: '500px' }],
98
98
  },
99
99
  },
100
+ order: {
101
+ editingRoute: {
102
+ component: 'edit-order-route-panel',
103
+ componentArguments: [{ isResizable: true }, { width: '500px' }],
104
+ },
105
+ },
100
106
  };
101
107
 
102
108
  /**
@@ -66,3 +66,8 @@ body[data-theme='dark'] .scheduler-left-sidebar {
66
66
  border: none;
67
67
  box-shadow: none;
68
68
  }
69
+
70
+ body[data-theme='dark'] .next-table-wrapper.order-view-purchase-rate-table-wrapper > table#order-view-purchase-rate-table tbody tr td {
71
+ background-color: rgb(31, 41, 55);
72
+ color: rgb(249, 250, 251);
73
+ }