From 02b1f190af66b02f30ba1e5a88f114d9ce83e5ee Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 15 Oct 2024 15:56:40 +0800 Subject: [PATCH 1/7] refactored the order list overlay component design and functionality, added ability to create a customer along with order in consumable api, few patches and fixes --- addon/components/live-map.hbs | 5 +- addon/components/live-map.js | 129 +++- .../order-config-manager/activity-flow.js | 5 +- .../order-config/entities-editor.js | 1 - .../components/order-config/fields-editor.js | 2 - addon/components/order-list-overlay.hbs | 279 ++++---- addon/components/order-list-overlay.js | 144 ++++- .../order-list-overlay/driver-panel-title.hbs | 13 +- addon/components/order-list-overlay/order.hbs | 123 +++- addon/components/order-list-overlay/order.js | 34 +- addon/controllers/operations/orders/index.js | 13 + .../operations/orders/index/new.js | 3 - addon/routes/operations/orders/index.js | 3 + addon/styles/fleetops-engine.css | 596 ++++++++++++++++++ addon/templates/operations/orders/index.hbs | 2 + addon/utils/is-facilitator-supported-place.js | 1 - composer.json | 2 +- extension.json | 2 +- package.json | 2 +- .../Controllers/Api/v1/ContactController.php | 27 +- .../Controllers/Api/v1/OrderController.php | 53 +- .../Internal/v1/FleetController.php | 52 ++ .../Internal/v1/LiveController.php | 52 +- server/src/Http/Filter/OrderFilter.php | 25 +- .../src/Http/Requests/CreateOrderRequest.php | 3 +- server/src/Http/Resources/v1/Driver.php | 19 +- server/src/Http/Resources/v1/Fleet.php | 2 +- server/src/Http/Resources/v1/Order.php | 4 +- .../src/Http/Resources/v1/TrackingNumber.php | 1 + server/src/Mail/CustomerCredentialsMail.php | 3 + server/src/Models/Driver.php | 3 + server/src/Models/Place.php | 2 +- server/src/Rules/CustomerIdOrDetails.php | 106 ++++ server/src/Support/OrderTracker.php | 20 +- translations/en-us.yaml | 4 +- 35 files changed, 1459 insertions(+), 276 deletions(-) create mode 100644 server/src/Rules/CustomerIdOrDetails.php diff --git a/addon/components/live-map.hbs b/addon/components/live-map.hbs index a3de2adf..b1eefb73 100644 --- a/addon/components/live-map.hbs +++ b/addon/components/live-map.hbs @@ -31,12 +31,13 @@ {{#if this.isDataLoaded}} {{#if this.visibilityControls.drivers}} {{#each this.drivers as |driver|}} + {{!-- {{log driver}} --}} { console.log('Data loaded', data); }, + * onLoaded: (data) => { debug('Data loaded', data); }, * onFailure: (error) => { console.error('Failed to load data', error); } * }); */ @@ -417,13 +423,13 @@ export default class LiveMapComponent extends Component { const callbackFnName = `on${internalName}Loaded`; const params = getWithDefault(options, 'params', {}); const url = `fleet-ops/live/${path}`; - const data = yield this.fetch.get(url, params, { normalizeToEmberData: true, normalizeModelType: singularize(internalName) }).catch((error) => { - if (typeof options.onFailure === 'function') { - options.onFailure(error); + + try { + let data = yield this.fetch.get(url, params, { normalizeToEmberData: true, normalizeModelType: singularize(internalName) }); + if (isArray(data)) { + data = [...data]; } - }); - if (data) { this.triggerAction(callbackFnName); this.createVisibilityControl(internalName); this[internalName] = data; @@ -432,9 +438,13 @@ export default class LiveMapComponent extends Component { if (typeof options.onLoaded === 'function') { options.onLoaded(data); } - } - return data; + return data; + } catch (error) { + if (typeof options.onFailure === 'function') { + options.onFailure(error); + } + } } /** @@ -1058,6 +1068,107 @@ export default class LiveMapComponent extends Component { }); } + @action previewOrderRoute(order) { + // Hide all elements on map + this.hideAll(); + + // Show drivers + this.show('drivers'); + + // create order route preview + const waypoints = this.getRouteCoordinatesFromOrder(order); + const routingHost = getRoutingHost(); + if (this.cannotRouteWaypoints(waypoints)) { + return; + } + + // center on first coordinate + try { + this.leafletMap.stop(); + this.leafletMap.flyTo(waypoints.firstObject); + } catch (error) { + // unable to stop map + debug(`Leaflet Map Error: ${error.message}`); + } + + const router = new OSRMv1({ + serviceUrl: `${routingHost}/route/v1`, + profile: 'driving', + }); + + this.routeControl = new RoutingControl({ + fitSelectedRoutes: false, + router, + waypoints, + alternativeClassName: 'hidden', + addWaypoints: false, + markerOptions: { + draggable: false, + icon: L.icon({ + iconUrl: '/assets/images/marker-icon.png', + iconRetinaUrl: '/assets/images/marker-icon-2x.png', + shadowUrl: '/assets/images/marker-shadow.png', + iconSize: [25, 41], + iconAnchor: [12, 41], + }), + }, + }).addTo(this.leafletMap); + + this.routeControl.on('routingerror', (error) => { + debug(`Routing Control Error: ${error.error.message}`); + }); + + this.routeControl.on('routesfound', () => { + this.leafletMap.flyToBounds(waypoints, { + paddingBottomRight: MAP_TARGET_FOCUS_PADDING_BOTTOM_RIGHT, + maxZoom: waypoints.length === 2 ? 13 : 12, + animate: true, + }); + this.leafletMap.once('moveend', () => { + this.leafletMap.panBy(MAP_TARGET_FOCUS_REFOCUS_PANBY); + }); + }); + } + + getRouteCoordinatesFromOrder(order) { + const payload = order.payload; + const waypoints = []; + const coordinates = []; + + waypoints.pushObjects([payload.pickup, ...payload.waypoints.toArray(), payload.dropoff]); + waypoints.forEach((place) => { + if (place && place.get('longitude') && place.get('latitude')) { + if (place.hasInvalidCoordinates) { + return; + } + + coordinates.pushObject([place.get('latitude'), place.get('longitude')]); + } + }); + + return coordinates; + } + + cannotRouteWaypoints(waypoints = []) { + return !this.leafletMap || !isArray(waypoints) || waypoints.length < 2; + } + + @action restoreDefaultLiveMap() { + this.removeRouteControl(); + this.showAll(); + this.leafletMap.flyTo([this.latitude, this.longitude], 13); + } + + removeRouteControl() { + if (this.routeControl && this.routeControl instanceof RoutingControl) { + try { + this.routeControl.remove(); + } catch (error) { + debug(`LiveMapComponent Error: ${error.message}`); + } + } + } + /** * Handle the creation of the draw control. * @@ -1689,7 +1800,7 @@ export default class LiveMapComponent extends Component { for await (let output of channel) { const { event, data } = output; - console.log(`[channel ${channelId}]`, output, event, data); + debug(`[channel ${channelId}]`, output, event, data); } })(); } diff --git a/addon/components/order-config-manager/activity-flow.js b/addon/components/order-config-manager/activity-flow.js index 4b4fc112..c65e52d7 100644 --- a/addon/components/order-config-manager/activity-flow.js +++ b/addon/components/order-config-manager/activity-flow.js @@ -6,6 +6,7 @@ import { inject as service } from '@ember/service'; import { action } from '@ember/object'; import { isArray } from '@ember/array'; import { later } from '@ember/runloop'; +import { debug } from '@ember/debug'; import { task } from 'ember-concurrency-decorators'; import generateUUID from '@fleetbase/ember-core/utils/generate-uuid'; import createFlowActivity from '../../utils/create-flow-activity'; @@ -462,8 +463,8 @@ export default class OrderConfigManagerActivityFlowComponent extends Component { let lastChildActivity = null; return activityObject.forEach((childActivityObject, childIndex) => { if (childIndex > 0 && lastChildActivity) { - console.log('[lastChildActivity]', lastChildActivity); - console.log('[childActivityObject]', childActivityObject); + debug('[lastChildActivity]', lastChildActivity); + debug('[childActivityObject]', childActivityObject); this.addNewLinkedActivity(lastChildActivity, childActivityObject); return; } diff --git a/addon/components/order-config/entities-editor.js b/addon/components/order-config/entities-editor.js index 118df828..389ca030 100644 --- a/addon/components/order-config/entities-editor.js +++ b/addon/components/order-config/entities-editor.js @@ -99,7 +99,6 @@ export default class OrderConfigEntitiesEditorComponent extends Component { const { orderConfig } = this; const { value } = target; - // console.log(value, get(orderConfig, `meta.entities.${index}.meta.${key}`)); set(orderConfig, `meta.entities.${index}.meta.${key}`, value); if (typeof this.args.onEntitiesChanged === 'function') { diff --git a/addon/components/order-config/fields-editor.js b/addon/components/order-config/fields-editor.js index cefffb32..0e6dad13 100644 --- a/addon/components/order-config/fields-editor.js +++ b/addon/components/order-config/fields-editor.js @@ -199,11 +199,9 @@ export default class OrderConfigFieldsEditorComponent extends Component { /* eslint no-unused-vars: "off" */ @action sortMetaFieldOptions(metaField, el, target) { - // console.log(`[sortMetaFieldOptions()]`, ...arguments); // const { fields } = this; // const { index } = el.dataset; // const parentEl = el.parentElement(); - // console.log(parentEl); // const { metaGroupKey } = target.dataset; // // get the index of the moved metafield // const metaFieldIndex = fields.findIndex((field) => field.key === metaFieldKey); diff --git a/addon/components/order-list-overlay.hbs b/addon/components/order-list-overlay.hbs index 9a95eb0e..48e65f11 100644 --- a/addon/components/order-list-overlay.hbs +++ b/addon/components/order-list-overlay.hbs @@ -1,9 +1,10 @@
- + {{#if this.load.isRunning}} + + {{else}} + + {{/if}}
- {{#if this.selectedOrders.length}} - +
+ {{#if this.selectedOrders.length}} + + +
+ +
+
+ +
+
+
+ {{t "fleet-ops.component.order-list-overlay.selected"}} + {{pluralize this.selectedOrders.length "Order"}} +
+
+
+ +
+
+
+ {{/if}} + -
- +
+
- {{/if}} - - -
- -
-
- - - -
- - - - {{#if (or this.loadFleets.isRunning this.loadUnassignedOrders.isRunning this.loadActiveOrders.isRunning)}} -
- - {{t "fleet-ops.common.loading"}} -
- {{/if}} - -
-
- - {{#each this.activeOrders as |order index|}} - - {{#if isSelected}} -
-
- {{!
-
- {{/if}} -
- {{/each}} -
- - {{#each this.unassignedOrders as |order index|}} - - {{#if isSelected}} -
-
- {{!
-
- {{/if}} -
- {{/each}} -
-
+ -
- {{#each this.fleets as |fleet|}} - - {{#each fleet.drivers as |driver|}} + +
+ {{#if this.load.isIdle}} +
+ {{#each-in this.orderGroups as |key orders|}} - {{#each driver.activeJobs as |order index|}} + {{#each orders as |order index|}} {{#if isSelected}} -
-
- {{!
+
+
{{/if}} {{/each}} + {{/each-in}} + + {{#each this.fleets as |fleet|}} + + {{#each fleet.drivers as |driver|}} + + {{#each driver.orderPanelActiveJobs as |order index|}} + + {{#if isSelected}} +
+
+ {{/if}} +
+ {{/each}} +
+ {{/each}} +
{{/each}} - - {{/each}} +
+ {{/if}} +
\ No newline at end of file diff --git a/addon/components/order-list-overlay.js b/addon/components/order-list-overlay.js index 2309e6b2..c80adcd1 100644 --- a/addon/components/order-list-overlay.js +++ b/addon/components/order-list-overlay.js @@ -2,6 +2,8 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; +import { isArray } from '@ember/array'; +import { isEmpty } from '@ember/utils'; import { task } from 'ember-concurrency'; import contextComponentCallback from '@fleetbase/ember-core/utils/context-component-callback'; @@ -9,29 +11,49 @@ export default class OrderListOverlayComponent extends Component { @service store; @service fetch; @service appCache; - @service router; @service hostRouter; @service notifications; @service abilities; + @service urlSearchParams; + @service contextPanel; @tracked fleets = []; - @tracked activeOrders = []; - @tracked unassignedOrders = []; @tracked selectedOrders = []; @tracked overlayContext; @tracked query = null; + @tracked isOpen = false; + @tracked loaded = false; + @tracked orderGroups = { + activeOrders: [], + unassignedOrders: [], + }; + + constructor(owner, { isOpen = false }) { + super(...arguments); + this.isOpen = isOpen; + } @action onLoad(overlayContext) { this.overlayContext = overlayContext; + if (this.urlSearchParams.get('orderPanelOpen')) { + this.overlayContext.open(); + } + if (typeof this.args.onLoad === 'function') { this.args.onLoad(...arguments); } } - @action onToggle() { - this.loadFleets.perform(); - this.loadUnassignedOrders.perform(); - this.loadActiveOrders.perform(); + @action onOpen() { + if (!this.loaded) { + this.load.perform(); + } + + this.urlSearchParams.addParamToCurrentUrl('orderPanelOpen', 1); + } + + @action onClose() { + this.urlSearchParams.removeParamFromCurrentUrl('orderPanelOpen'); } @action selectOrder(order) { @@ -43,23 +65,49 @@ export default class OrderListOverlayComponent extends Component { } @action viewOrder(order) { - const router = this.router ?? this.hostRouter; - - return router.transitionTo('console.fleet-ops.operations.orders.index.view', order); + return this.hostRouter.transitionTo('console.fleet-ops.operations.orders.index.view', order); } @action onAction(actionName, ...params) { contextComponentCallback(this, actionName, ...params, this); } + @task *load() { + yield this.loadFleets.perform(); + yield this.loadUnassignedOrders.perform(); + yield this.loadActiveOrders.perform(); + this.loaded = true; + } + @task *loadFleets() { if (this.abilities.cannot('fleet-ops list fleet')) { return; } + // Get orders which are already loaded to exclude from reloading + const activeLoadedOrders = this.getLoadedOrders(); + try { - this.fleets = yield this.store.query('fleet', { with: ['serviceArea', 'drivers.jobs', 'drivers.currentJob'], without: ['drivers.fleets'] }); - this.appCache.setEmberData('fleets', this.fleets); + let fleets = yield this.store.query('fleet', { + excludeDriverJobs: activeLoadedOrders.map((_) => _.public_id), + with: ['serviceArea', 'drivers.jobs', 'drivers.currentJob'], + without: ['drivers.fleets'], + }); + + // reset loaded jobs to drivers + if (isArray(fleets)) { + fleets = fleets.map((fleet) => { + fleet.drivers = fleet.drivers.map((driver) => { + driver.set('orderPanelActiveJobs', [...driver.activeJobs, ...this.getLoadedActiveOrderForDriver(driver)]); + return driver; + }); + + return fleet; + }); + + this.fleets = fleets; + this.appCache.setEmberData('fleets', fleets); + } } catch (error) { this.notifications.serverError(error); } @@ -70,8 +118,27 @@ export default class OrderListOverlayComponent extends Component { return; } + // Get orders which are already loaded to exclude from reloading + const activeLoadedOrders = this.getLoadedOrders(); + try { - this.unassignedOrders = yield this.store.query('order', { unassigned: 1 }); + const unassignedOrders = yield this.fetch.get( + 'fleet-ops/live/orders', + { + unassigned: 1, + exclude: activeLoadedOrders.map((_) => _.public_id), + }, + { + normalizeToEmberData: true, + normalizeModelType: 'order', + expirationInterval: 5, + expirationIntervalUnit: 'minute', + } + ); + this.orderGroups = { + ...this.orderGroups, + unassignedOrders: [...unassignedOrders, ...this.getLoadedUnassignedOrder()], + }; } catch (error) { this.notifications.serverError(error); } @@ -83,16 +150,15 @@ export default class OrderListOverlayComponent extends Component { } // Get orders which are already loaded to exclude from reloading - const loadedOrders = this.store.peekAll('order'); - const activeLoadedOrders = loadedOrders.filter((order) => { - return order.hasActiveStatus && order.has_driver_assigned; - }); + const activeLoadedOrders = this.getLoadedOrders(); // Load live orders try { - this.activeOrders = yield this.fetch.get( + const serverActiveOrders = yield this.fetch.get( 'fleet-ops/live/orders', { + active: 1, + with_tracker_data: 1, exclude: activeLoadedOrders.map((_) => _.public_id), }, { @@ -102,8 +168,50 @@ export default class OrderListOverlayComponent extends Component { expirationIntervalUnit: 'minute', } ); + const activeOrders = [...serverActiveOrders, ...this.getLoadedActiveOrder()]; + + for (let order of activeOrders) { + if (!order.get('tracker_data')) { + order.loadTrackerData(); + } + } + + this.orderGroups = { + ...this.orderGroups, + activeOrders, + }; } catch (error) { this.notifications.serverError(error); } } + + getLoadedOrders(filter = null) { + filter = + typeof filter === 'function' + ? filter + : function () { + return true; + }; + + const loadedOrders = this.store.peekAll('order'); + return loadedOrders.filter(filter); + } + + getLoadedUnassignedOrder() { + return this.getLoadedOrders((order) => { + return isEmpty(order.driver_assigned_uuid); + }); + } + + getLoadedActiveOrder() { + return this.getLoadedOrders((order) => { + return !isEmpty(order.driver_assigned) && !['created', 'completed', 'canceled', 'expired'].includes(order.status); + }); + } + + getLoadedActiveOrderForDriver(driver) { + return this.getLoadedOrders((order) => { + return !isEmpty(order.driver_assigned) && order.driver_assigned.id === driver.id && !['created', 'completed', 'canceled', 'expired'].includes(order.status); + }); + } } diff --git a/addon/components/order-list-overlay/driver-panel-title.hbs b/addon/components/order-list-overlay/driver-panel-title.hbs index 2d7eefff..c3efd6c3 100644 --- a/addon/components/order-list-overlay/driver-panel-title.hbs +++ b/addon/components/order-list-overlay/driver-panel-title.hbs @@ -1,6 +1,11 @@ -
- {{@context.name}} -
-
{{@context.name}}
+
+
+
+ {{@context.name}} +
+
{{@context.name}}
+
+
+ {{pluralize @context.orderPanelActiveJobs.length (t "fleet-ops.common.order")}}
\ No newline at end of file diff --git a/addon/components/order-list-overlay/order.hbs b/addon/components/order-list-overlay/order.hbs index a55bbee9..d7e486e3 100644 --- a/addon/components/order-list-overlay/order.hbs +++ b/addon/components/order-list-overlay/order.hbs @@ -1,27 +1,106 @@ -
- -
-
- {{@index}} + +
+
+
+
+ {{@index}} +
+
+ +
-
- +
+
{{@order.tracking}}
+ +
+
+
+ \ No newline at end of file diff --git a/addon/components/order-list-overlay/order.js b/addon/components/order-list-overlay/order.js index aff8a81d..8b2daf0f 100644 --- a/addon/components/order-list-overlay/order.js +++ b/addon/components/order-list-overlay/order.js @@ -1,3 +1,35 @@ import Component from '@glimmer/component'; +import { action } from '@ember/object'; -export default class OrderListOverlayOrderComponent extends Component {} +export default class OrderListOverlayOrderComponent extends Component { + @action onClick(order, event) { + //Don't run callback if action button is clicked + if (event.target.closest('span.order-listing-action-button')) { + event.stopPropagation(); + event.preventDefault(); + return; + } + + if (typeof this.args.onClick === 'function') { + this.args.onClick(...arguments); + } + } + + @action onDoubleClick() { + if (typeof this.args.onDoubleClick === 'function') { + this.args.onDoubleClick(...arguments); + } + } + + @action onMouseEnter() { + if (typeof this.args.onMouseEnter === 'function') { + this.args.onMouseEnter(...arguments); + } + } + + @action onMouseLeave() { + if (typeof this.args.onMouseLeave === 'function') { + this.args.onMouseLeave(...arguments); + } + } +} diff --git a/addon/controllers/operations/orders/index.js b/addon/controllers/operations/orders/index.js index 74f414e8..5b8daae4 100644 --- a/addon/controllers/operations/orders/index.js +++ b/addon/controllers/operations/orders/index.js @@ -49,6 +49,7 @@ export default class OperationsOrdersIndexController extends BaseController { 'layout', 'drawerOpen', 'drawerTab', + 'orderPanelOpen', ]; /** @@ -755,6 +756,18 @@ export default class OperationsOrdersIndexController extends BaseController { this.liveMap = liveMap; } + @action previewOrderRoute(order) { + if (this.liveMap) { + this.liveMap.previewOrderRoute(order); + } + } + + @action restoreDefaultLiveMap() { + if (this.liveMap) { + this.liveMap.restoreDefaultLiveMap(); + } + } + /** * Sets the drawer component context api. * diff --git a/addon/controllers/operations/orders/index/new.js b/addon/controllers/operations/orders/index/new.js index 53a85ad8..079add6e 100644 --- a/addon/controllers/operations/orders/index/new.js +++ b/addon/controllers/operations/orders/index/new.js @@ -874,9 +874,6 @@ export default class OperationsOrdersIndexNewController extends BaseController { profile: 'driving', }); - // console.log('[this.routePreviewArray]', this.routePreviewArray); - // console.log('[this.routePreviewCoordinates]', this.routePreviewCoordinates); - this.previewRouteControl = new RoutingControl({ waypoints: this.routePreviewCoordinates, alternativeClassName: 'hidden', diff --git a/addon/routes/operations/orders/index.js b/addon/routes/operations/orders/index.js index 9979bcde..1ba47dbe 100644 --- a/addon/routes/operations/orders/index.js +++ b/addon/routes/operations/orders/index.js @@ -26,6 +26,9 @@ export default class OperationsOrdersIndexRoute extends Route { before: { refreshModel: true }, type: { refreshModel: true }, layout: { refreshModel: false }, + drawerOpen: { refreshModel: false }, + drawerTab: { refreshModel: false }, + orderPanelOpen: { refreshModel: false }, }; @action willTransition(transition) { diff --git a/addon/styles/fleetops-engine.css b/addon/styles/fleetops-engine.css index c3b681da..7a53f772 100644 --- a/addon/styles/fleetops-engine.css +++ b/addon/styles/fleetops-engine.css @@ -992,3 +992,599 @@ body[data-theme='light'] .flb--modal.flb--default-modal.finalize-service-quote-p #fleetops-customer-orders-container.collapse-sidebar #fleetops-customer-orders-main-content { width: 100% !important; } + +.next-content-overlay-panel-body.fleetops-order-list-overlay { + padding: 0 !important; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay .fleetops-order-list-overlay-inner-wrapper .fleetops-order-list-overlay-list .next-content-panel-header { + padding: 0 0.35rem; + display: flex; + flex-direction: row; + border: 0; + border-bottom: 1px #e5e7eb solid; + border-radius: 0; + background-color: #fff; +} + +body[data-theme='dark'] .next-content-overlay-panel-body.fleetops-order-list-overlay .fleetops-order-list-overlay-inner-wrapper .fleetops-order-list-overlay-list .next-content-panel-header { + border-bottom: 1px #374151 solid; + background-color: #111827; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .fleetops-order-list-overlay-list + .next-content-panel-header + > .next-content-panel-header-left { + display: flex; + flex: 1; + width: 100%; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .fleetops-order-list-overlay-list + .next-content-panel-header + > .next-content-panel-header-left + > .icon-container { + font-size: 0.75rem; + width: 1rem; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .fleetops-order-list-overlay-list + .next-content-panel-header + > .next-content-panel-header-left + > .next-content-panel-title-container { + flex: 1; + width: 100%; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .fleetops-order-list-overlay-list + .next-content-panel-header + > .next-content-panel-header-left + > .next-content-panel-title-container + > .panel-title { + flex: 1; + width: 100%; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .fleetops-order-list-overlay-list + .next-content-panel-header + > .next-content-panel-header-left + > .next-content-panel-title-container + > .panel-title + > div { + flex: 1; + width: 100%; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .fleetops-order-list-overlay-list + .next-content-panel-header + > .next-content-panel-header-left + > .next-content-panel-title-container + > .panel-title + > div + > .resource-count { + font-size: 0.75rem; + border: 1px #d1d5db solid; + background-color: #fff; + padding: 0.1rem 0.5rem; + border-radius: 0.5rem; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 5%); + color: #111827; +} + +body[data-theme='dark'] + .next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .fleetops-order-list-overlay-list + .next-content-panel-header + > .next-content-panel-header-left + > .next-content-panel-title-container + > .panel-title + > div + > .resource-count { + border: 1px #4b5563 solid; + background-color: #1f2937; + color: #d1d5db; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .fleetops-order-list-overlay-list + .next-content-panel-header + > .next-content-panel-header-left + > .next-content-panel-title-container + > .panel-title + > div + > .fleetops-order-list-title-text { + font-size: 0.9rem; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .fleetops-order-list-overlay-list + .next-content-panel-header + > .next-content-panel-header-right { + display: none; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay .fleetops-order-list-overlay-inner-wrapper .next-content-panel-body.order-listings { + border-radius: 0; + padding: 0; + border: 0; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay .fleetops-order-list-overlay-inner-wrapper .next-content-panel-body.fleet-listings { + padding-left: 0.5rem; + border: 0; + background: transparent; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body.fleet-listings + > .next-content-panel-body-inner + > .next-content-panel-wrapper { + border-left: 1px #e5e7eb solid; +} + +body[data-theme='dark'] + .next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body.fleet-listings + > .next-content-panel-body-inner + > .next-content-panel-wrapper { + border-left: 1px #374151 solid; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body.fleet-listings + > .next-content-panel-body-inner + > .next-content-panel-wrapper + .next-content-panel-header { + padding-left: 1rem; + border: 0; + background: transparent; + border-bottom: 1px #e5e7eb solid; +} + +body[data-theme='dark'] + .next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body.fleet-listings + > .next-content-panel-body-inner + > .next-content-panel-wrapper + .next-content-panel-header { + border-bottom: 1px #374151 solid; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container { + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + background-color: #fff; + border-bottom: 1px #e5e7eb solid; + min-height: 196px; + color: #111827; +} + +body[data-theme='dark'] + .next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container { + background-color: #1f2937; + border-bottom: 1px #374151 solid; + color: #fff; +} + +body[data-theme='dark'] + .next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container.selected { + background-color: #1e3a8a; + border-bottom: 1px #1d4ed8 solid; + color: #fff; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container.selected { + background-color: #1e3a8a; + border-bottom: 1px #1d4ed8 solid; + color: #fff; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container:not(.selected):hover { + opacity: 0.75; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + .order-listing-actions { + display: flex; + flex-direction: row; + align-items: center; + padding: 0 0.5rem; + margin-bottom: 0.5rem; + width: 100%; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + .order-listing-actions + span.btn-wrapper { + box-shadow: none; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + .order-listing-actions + span.btn-wrapper + > button.btn { + border: 1px #1d4ed8 solid; + box-shadow: none; + background-color: #1e40af; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row { + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + flex-grow: 0; + align-items: flex-start; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + .order-listing-row-header { + display: flex; + flex-direction: row; + justify-content: space-between; + flex: 1; + width: 100%; + padding: 0.25rem 0.35rem; + background-color: #1d4ed8; + border-bottom: 1px #2563eb solid; + color: #fff; +} + +body[data-theme='dark'] + .next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + .order-listing-row-header { + background-color: #1e3a8a; + border-bottom: 1px #1d4ed8 solid; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-header + > .order-listing-row-prefix { + display: flex; + flex-direction: row; + line-height: 1.5rem; + font-size: 0.75rem; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-header + > .order-listing-row-prefix + > .order-listing-row-index { + width: 1.5rem; + display: flex; + justify-content: center; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + .order-listing-row-prefix + > .order-listing-row-icon-container { + width: 1.5rem; + display: flex; + justify-content: center; + align-items: center; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-header + > .order-listing-row-subheader { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + flex: 1; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-header + > .order-listing-row-subheader + > .order-listing-row-header-title { + font-size: 0.85rem; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details { + flex: 1; + width: 100%; + padding: 0 0.75rem 0.75rem; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-progress { + margin-bottom: 0.5rem; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-progress + .truck-icon-wrapper { + background-color: #fff; + color: #374151; +} + +body[data-theme='dark'] + .next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-progress + .truck-icon-wrapper { + background-color: #fff; + color: #374151; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-body { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 1rem; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-body + > .order-listing-row-body-address { + flex: 1; + font-size: 0.75rem; + line-height: 1.1rem; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-body + > .order-listing-row-body-address + .address-text { + max-height: 50px; + overflow-y: hidden; + text-overflow: ellipsis; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-body + > .order-listing-row-body-address.start { + text-align: left; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-body + > .order-listing-row-body-address.end { + text-align: right; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-footer + .resource-assigned { + display: flex; + flex-direction: row; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-footer { + margin-top: 0.5rem; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-footer + .resource-assigned + > .resource-assigned-photo { + display: flex; + flex-shrink: 0; + position: relative; + padding-top: 0.2rem; +} + +.next-content-overlay-panel-body.fleetops-order-list-overlay + .fleetops-order-list-overlay-inner-wrapper + .next-content-panel-body + > .next-content-panel-body-inner + .order-listings-row-container + > .order-listing-row + > .order-listing-row-details + > .order-listing-row-footer + .resource-assigned + > .resource-assigned-details { + display: flex; + flex-direction: column; + font-size: 0.75rem; + line-height: 1.1rem; +} + +.order-listing-row-table-details { + color: #d1d5db; + font-size: 0.75rem; + line-height: 1rem; + font-weight: normal; + text-align: left; + padding: 0.5rem; + width: 100%; + table-layout: fixed; +} + +.order-listing-row-table-details > thead > tr > th { + background-color: #111827; + color: #e5e7eb; + font-size: 0.75rem; + line-height: 1rem; + font-weight: normal; + text-align: left; + padding: 0.5rem; + border: 1px #374151 solid; + width: 50%; +} + +.order-listing-row-table-details > thead > tr > th:last-child { + border-left: 0; +} + +.order-listing-row-table-details > tbody > tr > td { + font-size: 0.75rem; + line-height: 1rem; + font-weight: normal; + text-align: left; + padding: 0.5rem; + vertical-align: top; +} diff --git a/addon/templates/operations/orders/index.hbs b/addon/templates/operations/orders/index.hbs index 85d3ede4..170f3d40 100644 --- a/addon/templates/operations/orders/index.hbs +++ b/addon/templates/operations/orders/index.hbs @@ -42,6 +42,8 @@ @onPressManageFleet={{this.fleetController.viewFleet}} @onPressCancelOrders={{this.bulkCancelOrders}} @onPressDeleteOrders={{this.bulkDeleteOrders}} + @onMouseEnterOrder={{this.previewOrderRoute}} + @onMouseLeaveOrder={{this.restoreDefaultLiveMap}} /> only(['name', 'type', 'title', 'email', 'phone', 'meta']); - // create the contact - $contact = Contact::updateOrCreate( - [ - 'company_uuid' => session('company'), - 'name' => strtoupper($input['name']), - ], - $input - ); + try { + // create the contact + $contact = Contact::updateOrCreate( + [ + 'company_uuid' => session('company'), + 'name' => strtoupper($input['name']), + ], + $input + ); + } catch (\Exception $e) { + return response()->apiError($e->getMessage()); + } // response the driver resource return new ContactResource($contact); @@ -93,12 +97,7 @@ public function find($id) try { $contact = Contact::findRecordOrFail($id); } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $exception) { - return response()->json( - [ - 'error' => 'Contact resource not found.', - ], - 404 - ); + return response()->apiError('Contact resource not found.', 404); } // response the contact resource diff --git a/server/src/Http/Controllers/Api/v1/OrderController.php b/server/src/Http/Controllers/Api/v1/OrderController.php index 21dd7cf6..3bef09f1 100644 --- a/server/src/Http/Controllers/Api/v1/OrderController.php +++ b/server/src/Http/Controllers/Api/v1/OrderController.php @@ -12,6 +12,7 @@ use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource; use Fleetbase\FleetOps\Http\Resources\v1\Order as OrderResource; use Fleetbase\FleetOps\Http\Resources\v1\Proof as ProofResource; +use Fleetbase\FleetOps\Models\Contact; use Fleetbase\FleetOps\Models\Driver; use Fleetbase\FleetOps\Models\Entity; use Fleetbase\FleetOps\Models\Order; @@ -28,6 +29,7 @@ use Fleetbase\Models\Setting; use Fleetbase\Support\Auth; use Illuminate\Http\Request; +use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; @@ -202,23 +204,50 @@ public function create(CreateOrderRequest $request) // customer assignment if ($request->has('customer')) { - $customer = Utils::getUuid( - ['contacts', 'vendors'], - [ - 'public_id' => $request->input('customer'), - 'company_uuid' => session('company'), - ] - ); + $customer = $request->input('customer'); - if (is_array($customer)) { - $input['customer_uuid'] = Utils::get($customer, 'uuid'); - $input['customer_type'] = Utils::getModelClassName(Utils::get($customer, 'table')); + if (is_string($customer)) { + $customer = Utils::getUuid( + ['contacts', 'vendors'], + [ + 'public_id' => $customer, + 'company_uuid' => session('company'), + ] + ); + + if (is_array($customer)) { + $input['customer_uuid'] = Utils::get($customer, 'uuid'); + $input['customer_type'] = Utils::getModelClassName(Utils::get($customer, 'table')); + } + } elseif (is_array($customer)) { + // create customer from input + $customer = Arr::only($customer, ['internal_id', 'name', 'title', 'email', 'phone', 'meta']); + + try { + $customer = Contact::firstOrCreate( + [ + 'email' => $customer['email'], + 'type' => 'customer', + ], + [ + ...$customer, + 'type' => 'customer', + ] + ); + } catch (\Exception $e) { + return response()->apiError('Failed to find or create customer for order.'); + } + + if ($customer instanceof Contact) { + $input['customer_uuid'] = $customer->uuid; + $input['customer_type'] = Utils::getModelClassName($customer); + } } } - // if no type is set its default to default + // if no type is set its default to transport if (!isset($input['type'])) { - $input['type'] = 'default'; + $input['type'] = 'transport'; } // if no status is set its default to `created` diff --git a/server/src/Http/Controllers/Internal/v1/FleetController.php b/server/src/Http/Controllers/Internal/v1/FleetController.php index 5d4d7947..d628ae31 100644 --- a/server/src/Http/Controllers/Internal/v1/FleetController.php +++ b/server/src/Http/Controllers/Internal/v1/FleetController.php @@ -13,6 +13,7 @@ use Fleetbase\FleetOps\Models\Vehicle; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; +use Illuminate\Support\Arr; use Illuminate\Support\Str; use Maatwebsite\Excel\Facades\Excel; @@ -25,6 +26,57 @@ class FleetController extends FleetOpsController */ public $resource = 'fleet'; + /** + * Query callback when querying record. + * + * @param \Illuminate\Database\Query\Builder $query + * @param \Illuminate\Http\Request $request + */ + public static function onQueryRecord($query, $request): void + { + if ($request->has('excludeDriverJobs')) { + $excludeJobs = $request->array('excludeDriverJobs'); + $query->with('drivers', function ($query) use ($excludeJobs) { + $query->with('jobs', function ($query) use ($excludeJobs) { + if (is_array($excludeJobs)) { + $isUuids = Arr::every($excludeJobs, function ($id) { + return Str::isUuid($id); + }); + + if ($isUuids) { + $query->whereNotIn('uuid', $excludeJobs); + } else { + $query->whereNotIn('public_id', $excludeJobs); + } + } + + $query->whereHas( + 'payload', + function ($q) { + $q->where( + function ($q) { + $q->whereHas('waypoints'); + $q->orWhereHas('pickup'); + $q->orWhereHas('dropoff'); + } + ); + $q->with(['entities', 'waypoints', 'dropoff', 'pickup', 'return']); + } + ); + $query->whereHas('trackingNumber'); + $query->whereHas('trackingStatuses'); + $query->with( + [ + 'payload', + 'trackingNumber', + 'trackingStatuses', + ] + ); + }); + }); + } + } + /** * Export the fleets to excel or csv. * diff --git a/server/src/Http/Controllers/Internal/v1/LiveController.php b/server/src/Http/Controllers/Internal/v1/LiveController.php index a23881a7..8ff026ae 100644 --- a/server/src/Http/Controllers/Internal/v1/LiveController.php +++ b/server/src/Http/Controllers/Internal/v1/LiveController.php @@ -73,16 +73,54 @@ function ($q) { */ public function orders(Request $request) { - $exclude = $request->array('exclude'); - - $orders = Order::where('company_uuid', session('company')) - ->whereHas('payload') - ->whereNotIn('status', ['canceled', 'completed']) + $exclude = $request->array('exclude'); + $active = $request->boolean('active'); + $unassigned = $request->boolean('unassigned'); + + $query = Order::where('company_uuid', session('company')) + ->whereHas('payload', function ($query) { + $query->where( + function ($q) { + $q->whereHas('waypoints'); + $q->orWhereHas('pickup'); + $q->orWhereHas('dropoff'); + } + ); + $query->with(['entities', 'waypoints', 'dropoff', 'pickup', 'return']); + }) + ->whereNotIn('status', ['canceled', 'completed', 'expired']) + ->whereHas('trackingNumber') + ->whereHas('trackingStatuses') ->whereNotIn('public_id', $exclude) - ->whereNotNull('driver_assigned_uuid') ->whereNull('deleted_at') ->applyDirectivesForPermissions('fleet-ops list order') - ->get(); + ->with(['payload', 'trackingNumber', 'trackingStatuses']); + + if ($active) { + $query->whereHas('driverAssigned'); + } + + if ($unassigned) { + $query->whereNull('driver_assigned_uuid'); + } + + $orders = $query->get(); + + // Get additional data or load missing if necessary + $orders->map( + function ($order) use ($request) { + // load required relations + $order->loadMissing(['trackingNumber', 'payload', 'trackingStatuses']); + + // load tracker data + if ($request->has('with_tracker_data')) { + $order->tracker_data = $order->tracker()->toArray(); + $order->eta = $order->tracker()->eta(); + } + + return $order; + } + ); return OrderResource::collection($orders); } diff --git a/server/src/Http/Filter/OrderFilter.php b/server/src/Http/Filter/OrderFilter.php index f4406406..eeffce3b 100644 --- a/server/src/Http/Filter/OrderFilter.php +++ b/server/src/Http/Filter/OrderFilter.php @@ -2,8 +2,10 @@ namespace Fleetbase\FleetOps\Http\Filter; +use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Filter\Filter; use Fleetbase\Support\Http; +use Illuminate\Support\Arr; use Illuminate\Support\Str; class OrderFilter extends Filter @@ -32,6 +34,7 @@ function ($q) { 'payload', 'trackingNumber', 'trackingStatuses', + 'driverAssigned', ] ); } @@ -68,7 +71,7 @@ public function unassigned(bool $unassigned) if ($unassigned) { $this->builder->where( function ($q) { - $q->whereNull('driver_assigned_uuid'); + $q->whereDoesntHave('driverAssigned'); $q->whereNotIn('status', ['completed', 'canceled', 'expired']); } ); @@ -90,8 +93,8 @@ public function active(bool $active = false) if ($active) { $this->builder->where( function ($q) { + $q->whereHas('driverAssigned'); $q->whereNotIn('status', ['created', 'dispatched', 'pending', 'canceled', 'completed']); - $q->whereNotNull('driver_assigned_uuid'); } ); } @@ -234,6 +237,8 @@ function ($query) use ($return) { public function driver(string $driver) { + $this->builder->with('driverAssigned'); + if (Str::isUuid($driver)) { $this->builder->where('driver_assigned_uuid', $driver); } else { @@ -294,4 +299,20 @@ public function sort(string $sort) return $this->builder; } + + public function exclude($exclude) + { + $exclude = Utils::arrayFrom($exclude); + if (is_array($exclude)) { + $isUuids = Arr::every($exclude, function ($id) { + return Str::isUuid($id); + }); + + if ($isUuids) { + $this->builder->whereNotIn('uuid', $exclude); + } else { + $this->builder->whereNotIn('public_id', $exclude); + } + } + } } diff --git a/server/src/Http/Requests/CreateOrderRequest.php b/server/src/Http/Requests/CreateOrderRequest.php index 593913fb..bb365761 100644 --- a/server/src/Http/Requests/CreateOrderRequest.php +++ b/server/src/Http/Requests/CreateOrderRequest.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Requests; +use Fleetbase\FleetOps\Rules\CustomerIdOrDetails; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\FleetbaseRequest; use Fleetbase\Rules\ExistsInAny; @@ -36,7 +37,7 @@ public function rules() 'service_quote' => ['nullable', 'exists:service_quotes,public_id'], 'purchase_rate' => ['nullable', 'exists:purchase_rates,public_id'], 'facilitator' => ['nullable', new ExistsInAny(['vendors', 'contacts', 'integrated_vendors'], ['public_id', 'provider'])], - 'customer' => ['nullable', new ExistsInAny(['vendors', 'contacts'], 'public_id')], + 'customer' => ['nullable', new CustomerIdOrDetails(['vendors', 'contacts'], 'public_id')], 'status' => ['nullable', 'string'], 'type' => ['string'], ]; diff --git a/server/src/Http/Resources/v1/Driver.php b/server/src/Http/Resources/v1/Driver.php index c4cd5a70..5a3722a5 100644 --- a/server/src/Http/Resources/v1/Driver.php +++ b/server/src/Http/Resources/v1/Driver.php @@ -4,9 +4,11 @@ use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Resources\FleetbaseResource; +use Fleetbase\Http\Resources\FleetbaseResourceCollection; use Fleetbase\Http\Resources\User; use Fleetbase\LaravelMysqlSpatial\Types\Point; use Fleetbase\Support\Http; +use Illuminate\Http\Resources\Json\AnonymousResourceCollection; class Driver extends FleetbaseResource { @@ -41,9 +43,9 @@ public function toArray($request) 'vehicle_avatar' => $this->when(Http::isInternalRequest(), $this->vehicle_avatar), 'vendor_name' => $this->when(Http::isInternalRequest(), $this->vendor_name), 'vehicle' => $this->whenLoaded('vehicle', fn () => new VehicleWithoutDriver($this->vehicle)), - 'current_job' => $this->whenLoaded('currentJob', fn () => new CurrentJob($this->currentJob)), + 'current_job' => $this->whenLoaded('currentJob', fn () => new Order($this->currentJob)), 'current_job_id' => $this->when(Http::isInternalRequest(), data_get($this, 'currentJob.public_id')), - 'jobs' => $this->whenLoaded('jobs', fn () => CurrentJob::collection($this->jobs()->without(['driverAssigned'])->get())), + 'jobs' => $this->whenLoaded('jobs', fn () => $this->getJobs()), 'vendor' => $this->whenLoaded('vendor', fn () => new Vendor($this->vendor)), 'fleets' => $this->whenLoaded('fleets', fn () => Fleet::collection($this->fleets()->without('drivers')->get())), 'location' => $this->wasRecentlyCreated ? new Point(0, 0) : data_get($this, 'location', new Point(0, 0)), @@ -62,6 +64,19 @@ public function toArray($request) ]; } + public function getJobs(): AnonymousResourceCollection|FleetbaseResourceCollection + { + return Order::collection( + $this->jobs()->with( + [ + 'driverAssigned' => function ($query) { + $query->without('jobs'); + }, + ] + )->get() + ); + } + /** * Transform the resource into an webhook payload. * diff --git a/server/src/Http/Resources/v1/Fleet.php b/server/src/Http/Resources/v1/Fleet.php index fe355a4b..0676d2b0 100644 --- a/server/src/Http/Resources/v1/Fleet.php +++ b/server/src/Http/Resources/v1/Fleet.php @@ -41,7 +41,7 @@ public function toArray($request) 'vendor' => $this->whenLoaded('vendor', fn () => new Vendor($this->vendor)), 'parent_fleet' => $this->whenLoaded('parentFleet', fn () => new ParentFleet($this->parentFleet)), 'subfleets' => $this->whenLoaded('subFleets', fn () => SubFleet::collection($this->subFleets)), - 'drivers' => $this->whenLoaded('drivers', fn () => Driver::collection($this->drivers()->without(['driverAssigned'])->with(Http::isInternalRequest() ? ['jobs'] : [])->get())), + 'drivers' => $this->whenLoaded('drivers', fn () => Driver::collection($this->drivers()->with(Http::isInternalRequest() || $request->has('with.jobs') ? ['jobs'] : [])->get())), 'vehicles' => $this->whenLoaded('vehicles', fn () => Vehicle::collection($this->vehicles)), 'updated_at' => $this->updated_at, 'created_at' => $this->created_at, diff --git a/server/src/Http/Resources/v1/Order.php b/server/src/Http/Resources/v1/Order.php index 74b452ed..c8d93552 100644 --- a/server/src/Http/Resources/v1/Order.php +++ b/server/src/Http/Resources/v1/Order.php @@ -67,8 +67,8 @@ public function toArray($request) 'adhoc_distance' => (int) $this->getAdhocDistance(), 'distance' => (int) $this->distance, 'time' => (int) $this->time, - 'tracker_data' => $this->when($request->has('with_tracker_data') || !empty($this->resource->tracker_data), fn () => $this->resource->tracker_data ?? $this->tracker()->toArray()), - 'eta' => $this->when($request->has('with_eta') || !empty($this->resource->eta), fn () => $this->resource->eta ?? $this->tracker()->eta()), + 'tracker_data' => $this->when($request->has('with_tracker_data') || !empty($this->resource->tracker_data), fn () => $this->resource->tracker_data ?? $this->resource->tracker()->toArray()), + 'eta' => $this->when($request->has('with_eta') || !empty($this->resource->eta), fn () => $this->resource->eta ?? $this->resource->tracker()->eta()), 'meta' => data_get($this, 'meta', []), 'dispatched_at' => $this->dispatched_at, 'started_at' => $this->started_at, diff --git a/server/src/Http/Resources/v1/TrackingNumber.php b/server/src/Http/Resources/v1/TrackingNumber.php index 185f5fed..c8268625 100644 --- a/server/src/Http/Resources/v1/TrackingNumber.php +++ b/server/src/Http/Resources/v1/TrackingNumber.php @@ -31,6 +31,7 @@ public function toArray($request) 'status_code' => $this->last_status_code, 'qr_code' => $this->qr_code, 'barcode' => $this->barcode, + 'url' => Utils::consoleUrl('track-order', ['order' => $this->tracking_number]), 'type' => Utils::getTypeFromClassName($this->owner_type), 'updated_at' => $this->updated_at, 'created_at' => $this->created_at, diff --git a/server/src/Mail/CustomerCredentialsMail.php b/server/src/Mail/CustomerCredentialsMail.php index e9187cb1..a37ef558 100644 --- a/server/src/Mail/CustomerCredentialsMail.php +++ b/server/src/Mail/CustomerCredentialsMail.php @@ -54,6 +54,9 @@ public function envelope(): Envelope */ public function content(): Content { + $user = $this->customer->getUser(); + $this->customer->setRelation('user', $user); + return new Content( markdown: 'fleetops::mail.customer-credentials', with: [ diff --git a/server/src/Models/Driver.php b/server/src/Models/Driver.php index 2aaf7058..ab4b201f 100644 --- a/server/src/Models/Driver.php +++ b/server/src/Models/Driver.php @@ -218,6 +218,9 @@ public function vehicle() { return $this->belongsTo(Vehicle::class)->select([ 'uuid', + 'vendor_uuid', + 'photo_uuid', + 'avatar_url', 'public_id', 'year', 'make', diff --git a/server/src/Models/Place.php b/server/src/Models/Place.php index 6b43a531..109adcd4 100644 --- a/server/src/Models/Place.php +++ b/server/src/Models/Place.php @@ -551,7 +551,7 @@ public static function insertFromMixed($place) { if (Utils::isCoordinatesStrict($place)) { // create a place from coordinates using reverse loopup - return Place::insertFromCoordinates($place, true); + return Place::insertFromCoordinates($place); } elseif (is_string($place)) { if (Utils::isPublicId($place)) { $resolvedPlace = Place::where('public_id', $place)->first(); diff --git a/server/src/Rules/CustomerIdOrDetails.php b/server/src/Rules/CustomerIdOrDetails.php new file mode 100644 index 00000000..62ce01f0 --- /dev/null +++ b/server/src/Rules/CustomerIdOrDetails.php @@ -0,0 +1,106 @@ +tables = $tables; + $this->column = $column; + $this->message = 'The :attribute is invalid.'; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * + * @return bool + */ + public function passes($attribute, $value) + { + // If the value is a string, validate as customer ID + if (is_string($value)) { + $existsRule = new ExistsInAny($this->tables, $this->column); + + return $existsRule->passes($attribute, $value); + } + + // If the value is an array, validate the structure + if (is_array($value)) { + // Check for at least 'name' or 'email' + if (!array_key_exists('name', $value) && !array_key_exists('email', $value)) { + $this->message = 'The :attribute must have at least a name and email.'; + + return false; + } + + // If 'email' is present, validate its format + if (array_key_exists('email', $value)) { + if (!filter_var($value['email'], FILTER_VALIDATE_EMAIL)) { + $this->message = 'The :attribute email must be a valid email address.'; + + return false; + } + } + + // If 'name' is present, ensure it's a non-empty string + if (array_key_exists('name', $value)) { + if (!is_string($value['name']) || empty(trim($value['name']))) { + $this->message = 'The :attribute name must be a non-empty string.'; + + return false; + } + } + + // Optionally, you can add more validations for the array here + + return true; + } + + // If the value is neither a string nor an array, fail validation + $this->message = 'The :attribute must be either a string (customer ID) or an object with name and/or email.'; + + return false; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return $this->message; + } +} diff --git a/server/src/Support/OrderTracker.php b/server/src/Support/OrderTracker.php index 22b04d52..c3939897 100644 --- a/server/src/Support/OrderTracker.php +++ b/server/src/Support/OrderTracker.php @@ -78,6 +78,10 @@ public function getOrderProgressPercentage(): int|float $totalDistance = $this->getTotalDistance(); $completedDistance = $this->getCompletedDistance(); + if ($totalDistance === -1 || $completedDistance === -1) { + return 0; + } + // Get order percentage by activity if distance-based progress is not available $shouldCalculateProgressByActivity = empty($completedDistance) && $this->order->status !== 'created'; if ($shouldCalculateProgressByActivity) { @@ -112,6 +116,10 @@ public function getOrderProgressPercentage(): int|float public function getTotalDistance(): int|float { $points = $this->getAllDestinationPoints()->toArray(); + if (count($points) < 2) { + return -1; + } + $response = OSRM::getRouteFromPoints($points); if (isset($response['code']) && $response['code'] === 'Ok') { $route = Arr::first($response['routes']); @@ -120,7 +128,7 @@ public function getTotalDistance(): int|float } } - return 0; + return -1; } /** @@ -132,7 +140,7 @@ public function getCompletedDistance(): float { $points = $this->getCompletedDestinationPoints()->toArray(); if (count($points) < 2) { - return 0; + return -1; } $response = OSRM::getRouteFromPoints($points); @@ -143,7 +151,7 @@ public function getCompletedDistance(): float } } - return 0; + return -1; } /** @@ -155,7 +163,7 @@ public function getCurrentDestinationETA(): float { $start = $this->getDriverCurrentLocation(); $currentDestination = $this->getCurrentDestination(); - $end = $currentDestination->location; + $end = $currentDestination ? $currentDestination->location : null; if ($start == $end) { $nextDestination = $this->getNextDestination(); if ($nextDestination) { @@ -163,6 +171,10 @@ public function getCurrentDestinationETA(): float } } + if (!$start || !$end) { + return -1; + } + $response = OSRM::getRoute($start, $end); if (isset($response['code']) && $response['code'] === 'Ok') { $route = Arr::first($response['routes']); diff --git a/translations/en-us.yaml b/translations/en-us.yaml index af249cfc..b9badb9e 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -1042,8 +1042,8 @@ fleet-ops: actions: Actions create-order: Create new order... create-fleet: Create new fleet... - active-order: Active orders - unassigned-order: Unassigned orders + active-orders: Active Orders + unassigned-orders: Unassigned Orders place-form-panel: success-message: place ({placeAddress}) saved successfully. place: New Place From 7488a0655c2645a296cf0932f79244671751b9a0 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 15 Oct 2024 16:15:51 +0800 Subject: [PATCH 2/7] remove template log --- addon/components/live-map.hbs | 1 - 1 file changed, 1 deletion(-) diff --git a/addon/components/live-map.hbs b/addon/components/live-map.hbs index b1eefb73..557b86fe 100644 --- a/addon/components/live-map.hbs +++ b/addon/components/live-map.hbs @@ -31,7 +31,6 @@ {{#if this.isDataLoaded}} {{#if this.visibilityControls.drivers}} {{#each this.drivers as |driver|}} - {{!-- {{log driver}} --}} Date: Tue, 15 Oct 2024 16:32:52 +0800 Subject: [PATCH 3/7] patch vehicle assignment to driver from vehicle update --- server/src/Models/Driver.php | 44 ++++++++++++++++++++---- server/src/Observers/VehicleObserver.php | 4 +-- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/server/src/Models/Driver.php b/server/src/Models/Driver.php index ab4b201f..74469e2e 100644 --- a/server/src/Models/Driver.php +++ b/server/src/Models/Driver.php @@ -509,22 +509,52 @@ public function unassignCurrentOrder() } /** - * Assign a vehicle to driver. + * Assigns the specified vehicle to the current driver. * - * @return void + * This method performs the following actions: + * 1. Unassigns the vehicle from any other drivers by setting their `vehicle_uuid` to `null`. + * 2. Assigns the vehicle to the current driver by updating the vehicle's `driver_uuid`. + * 3. Associates the vehicle with the current driver instance. + * 4. Saves the changes to persist the assignment. + * + * @param Vehicle $vehicle the vehicle instance to assign to the driver + * + * @return $this returns the current driver instance after assignment + * + * @throws \Exception if the vehicle assignment fails */ - public function assignVehicle(Vehicle $vehicle) + public function assignVehicle(Vehicle $vehicle): self { - // auto: unassign vehicle from other drivers + // Unassign vehicle from other drivers static::where('vehicle_uuid', $vehicle->uuid)->update(['vehicle_uuid' => null]); - // assign driver to vehicle + // Assign driver to vehicle $vehicle->update(['driver_uuid' => $this->uuid]); - // set this vehicle + // Set this vehicle to the driver instance + $this->setVehicle($vehicle); + $this->save(); + + return $this; + } + + /** + * Sets the vehicle for the current driver instance. + * + * This method updates the `vehicle_uuid` attribute of the driver and establishes + * the relationship between the driver and the vehicle model instance. + * + * @param Vehicle $vehicle the vehicle instance to associate with the driver + * + * @return $this returns the current driver instance after setting the vehicle + */ + public function setVehicle(Vehicle $vehicle) + { + // Update the driver's vehicle UUID $this->vehicle_uuid = $vehicle->uuid; + + // Establish the relationship with the vehicle $this->setRelation('vehicle', $vehicle); - $this->save(); return $this; } diff --git a/server/src/Observers/VehicleObserver.php b/server/src/Observers/VehicleObserver.php index 1eb472ed..13e975a3 100644 --- a/server/src/Observers/VehicleObserver.php +++ b/server/src/Observers/VehicleObserver.php @@ -35,7 +35,7 @@ public function created(Vehicle $vehicle) * * @return void */ - public function updated(Vehicle $vehicle) + public function updating(Vehicle $vehicle) { // assign this vehicle to a driver if the driver has been set $identifier = request()->or(['driver_uuid', 'vehicle.driver_uuid', 'vehicle.driver.uuid']); @@ -45,7 +45,7 @@ public function updated(Vehicle $vehicle) if ($driver) { // assign this vehicle to driver - $driver->assignVehicle($vehicle); + $driver->setVehicle($vehicle); // set driver to vehicle $vehicle->setRelation('driver', $driver); From a2af281ccdf074ca8468f2f5a2e5cf0fa7a6eb92 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 15 Oct 2024 16:42:42 +0800 Subject: [PATCH 4/7] patch driver assignment from vehicle update --- server/src/Models/Driver.php | 3 --- server/src/Observers/VehicleObserver.php | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/Models/Driver.php b/server/src/Models/Driver.php index 74469e2e..23732374 100644 --- a/server/src/Models/Driver.php +++ b/server/src/Models/Driver.php @@ -528,9 +528,6 @@ public function assignVehicle(Vehicle $vehicle): self // Unassign vehicle from other drivers static::where('vehicle_uuid', $vehicle->uuid)->update(['vehicle_uuid' => null]); - // Assign driver to vehicle - $vehicle->update(['driver_uuid' => $this->uuid]); - // Set this vehicle to the driver instance $this->setVehicle($vehicle); $this->save(); diff --git a/server/src/Observers/VehicleObserver.php b/server/src/Observers/VehicleObserver.php index 13e975a3..c0515621 100644 --- a/server/src/Observers/VehicleObserver.php +++ b/server/src/Observers/VehicleObserver.php @@ -45,7 +45,7 @@ public function updating(Vehicle $vehicle) if ($driver) { // assign this vehicle to driver - $driver->setVehicle($vehicle); + $driver->assignVehicle($vehicle, false); // set driver to vehicle $vehicle->setRelation('driver', $driver); From c708e09215c86b672ff73b35a7d29a08fdff6bee Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 15 Oct 2024 17:04:42 +0800 Subject: [PATCH 5/7] fix tracking label box in order view --- addon/templates/operations/orders/index/view.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/templates/operations/orders/index/view.hbs b/addon/templates/operations/orders/index/view.hbs index 01c11d0c..93640151 100644 --- a/addon/templates/operations/orders/index/view.hbs +++ b/addon/templates/operations/orders/index/view.hbs @@ -479,7 +479,7 @@
{{/if}} -
+
{{@model.public_id}} From eb1ec090f90bcd51b93bbcc6e8f35f4e9080cb5a Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 15 Oct 2024 17:17:02 +0800 Subject: [PATCH 6/7] upgraded ui dependency --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 5e228e91..d69a1255 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "dependencies": { "@babel/core": "^7.23.2", "@fleetbase/ember-core": "^0.2.21", - "@fleetbase/ember-ui": "^0.2.34", + "@fleetbase/ember-ui": "^0.2.35", "@fleetbase/fleetops-data": "^0.1.18", "@fleetbase/leaflet-routing-machine": "^3.2.16", "@fortawesome/ember-fontawesome": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 261c4fd8..f1df8b2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^0.2.21 version: 0.2.21(@ember/string@3.1.1)(@ember/test-helpers@3.3.1(@babel/core@7.25.2)(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(webpack@5.94.0))(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0)))(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(eslint@8.57.0)(webpack@5.94.0) '@fleetbase/ember-ui': - specifier: ^0.2.34 - version: 0.2.34(@ember/test-helpers@3.3.1(@babel/core@7.25.2)(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(webpack@5.94.0))(@glimmer/component@1.1.2(@babel/core@7.25.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0)))(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(postcss@8.4.47)(rollup@2.79.1)(tracked-built-ins@3.3.0)(webpack@5.94.0) + specifier: ^0.2.35 + version: 0.2.35(@ember/test-helpers@3.3.1(@babel/core@7.25.2)(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(webpack@5.94.0))(@glimmer/component@1.1.2(@babel/core@7.25.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0)))(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(postcss@8.4.47)(rollup@2.79.1)(tracked-built-ins@3.3.0)(webpack@5.94.0) '@fleetbase/fleetops-data': specifier: ^0.1.18 version: 0.1.18(@ember/string@3.1.1)(@ember/test-helpers@3.3.1(@babel/core@7.25.2)(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(webpack@5.94.0))(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0)))(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(eslint@8.57.0)(webpack@5.94.0) @@ -1390,8 +1390,8 @@ packages: resolution: {integrity: sha512-Jwe4ME+cJp24Oaf6wfd1QJK6g3D6/Ff+qUMinJ40yoMImpo3zKA+IAxfQSBPIf+9BrmNG31wquTz4WR7Um47LQ==} engines: {node: '>= 18'} - '@fleetbase/ember-ui@0.2.34': - resolution: {integrity: sha512-9uLCufgIaMFgGR+G4m73shaiRz+F16CDs7jj4jFzQGSUQByaBcbSetiMoGXSnx+3IncaKAn8rjAVPd+QrDDRMg==} + '@fleetbase/ember-ui@0.2.35': + resolution: {integrity: sha512-diJRlY92LTSFhf2bMlO2j8uyVabeTTC5YHuRqYYpTHk6GjLcVHUVeL4MrLnskKzkkbRsAgmoavkW9HcXNBN8Ow==} engines: {node: '>= 18'} '@fleetbase/fleetops-data@0.1.18': @@ -9845,7 +9845,7 @@ snapshots: - utf-8-validate - webpack - '@fleetbase/ember-ui@0.2.34(@ember/test-helpers@3.3.1(@babel/core@7.25.2)(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(webpack@5.94.0))(@glimmer/component@1.1.2(@babel/core@7.25.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0)))(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(postcss@8.4.47)(rollup@2.79.1)(tracked-built-ins@3.3.0)(webpack@5.94.0)': + '@fleetbase/ember-ui@0.2.35(@ember/test-helpers@3.3.1(@babel/core@7.25.2)(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(webpack@5.94.0))(@glimmer/component@1.1.2(@babel/core@7.25.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0)))(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0))(postcss@8.4.47)(rollup@2.79.1)(tracked-built-ins@3.3.0)(webpack@5.94.0)': dependencies: '@babel/core': 7.25.2 '@ember/render-modifiers': 2.1.0(@babel/core@7.25.2)(ember-source@5.4.1(@babel/core@7.25.2)(@glimmer/component@1.1.2(@babel/core@7.25.2))(rsvp@4.8.5)(webpack@5.94.0)) From 4dc7c8f30dc18eb9dd82fdab5be7e0734c234529 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 15 Oct 2024 17:30:48 +0800 Subject: [PATCH 7/7] patch tracker order progress calculation --- server/src/Support/OrderTracker.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/server/src/Support/OrderTracker.php b/server/src/Support/OrderTracker.php index c3939897..eb6b061a 100644 --- a/server/src/Support/OrderTracker.php +++ b/server/src/Support/OrderTracker.php @@ -77,14 +77,10 @@ public function getOrderProgressPercentage(): int|float { $totalDistance = $this->getTotalDistance(); $completedDistance = $this->getCompletedDistance(); - - if ($totalDistance === -1 || $completedDistance === -1) { - return 0; - } + $cannotUseDistance = $totalDistance == -1 || $completedDistance == -1 || $completedDistance === 0; // Get order percentage by activity if distance-based progress is not available - $shouldCalculateProgressByActivity = empty($completedDistance) && $this->order->status !== 'created'; - if ($shouldCalculateProgressByActivity) { + if ($cannotUseDistance) { /** @var Collection $activities */ $activities = $this->order->orderConfig ? $this->order->orderConfig->activities() : collect(); $totalActivity = $activities->count();