@ducna01120/fleetops-engine
Version:
Fleet & Transport Management Extension for Fleetbase
1,505 lines (1,261 loc) • 53.7 kB
JavaScript
import BaseController from '@ducna01120/fleetops-engine/controllers/base-controller';
import { inject as controller } from '@ember/controller';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action, computed, setProperties, set, get } from '@ember/object';
import { not, equal, alias } from '@ember/object/computed';
import { isArray } from '@ember/array';
import { isBlank } from '@ember/utils';
import { dasherize } from '@ember/string';
import { later, next } from '@ember/runloop';
import { task } from 'ember-concurrency-decorators';
import { OSRMv1, Control as RoutingControl } from '@fleetbase/leaflet-routing-machine';
import polyline from '@fleetbase/ember-core/utils/polyline';
import findClosestWaypoint from '@fleetbase/ember-core/utils/find-closest-waypoint';
import isNotEmpty from '@fleetbase/ember-core/utils/is-not-empty';
import getRoutingHost from '@fleetbase/ember-core/utils/get-routing-host';
import getWithDefault from '@fleetbase/ember-core/utils/get-with-default';
import isModel from '@fleetbase/ember-core/utils/is-model';
L.Bounds.prototype.intersects = function (bounds) {
var min = this.min,
max = this.max,
min2 = bounds.min,
max2 = bounds.max,
xIntersects = max2.x >= min.x && min2.x <= max.x,
yIntersects = max2.y >= min.y && min2.y <= max.y;
return xIntersects && yIntersects;
};
export default class OperationsOrdersIndexNewController extends BaseController {
@controller('operations.orders.index') ordersController;
/**
* Inject the `modalsManager` service
*
* @var {Service}
*/
@service modalsManager;
/**
* Inject the `notifications` service
*
* @var {Service}
*/
@service notifications;
/**
* Inject the `loader` service
*
* @var {Service}
*/
@service loader;
/**
* Inject the `currentUser` service
*
* @var {Service}
*/
@service currentUser;
/**
* Inject the `hostRouter` service
*
* @var {Service}
*/
@service hostRouter;
/**
* Inject the `fileQueue` service
*
* @var {Service}
*/
@service fileQueue;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* Inject the `fetch` service
*
* @var {Service}
*/
@service fetch;
/**
* Inject the `store` service
*
* @var {Service}
*/
@service store;
/**
* Inject the `contextPanel` service
*
* @var {Service}
*/
@service contextPanel;
/**
* Inject the `universe` service
*
* @var {Service}
*/
@service universe;
/**
* Create an OrderModel instance.
*
* @var {OrderModel}
*/
@tracked order = this.store.createRecord('order', { meta: [] });
/**
* Create an PayloadModel instance.
*
* @var {OrderModel}
*/
@tracked payload = this.store.createRecord('payload');
@tracked driversQuery = {};
@tracked vehiclesQuery = {};
@tracked meta = [];
@tracked entities = [];
@tracked waypoints = [];
@tracked payloadCoordinates = [];
@tracked orderConfig;
@tracked orderConfigs = [];
@tracked customFieldGroups = [];
@tracked customFields = [];
@tracked customFieldValues = {};
@tracked serviceRates = [];
@tracked selectedServiceRate;
@tracked selectedServiceQuote;
@tracked isCustomFieldsValid = true;
@tracked isCreatingOrder = false;
@tracked isMultipleDropoffOrder = false;
@tracked isViewingRoutePreview = false;
@tracked isOptimizingRoute = false;
@tracked optimizedRouteMarkers = [];
@tracked optimizedRoutePolyline;
@tracked isFetchingQuotes = false;
@tracked servicable = false;
@tracked scheduledDate;
@tracked scheduledTime;
@tracked leafletRoute;
@tracked leafletOptimizedRoute;
@tracked currentLeafletRoute;
@tracked leafletLayers = [];
@tracked routeProfile = 'driving';
@tracked routeProfileOptions = ['driving', 'bycicle', 'walking'];
@tracked podOptions = ['scan', 'signature', 'photo'];
@tracked isCsvImportedOrder = false;
@tracked routePreviewArray = [];
@tracked previewRouteControl;
@tracked isSubscriptionValid = true;
@tracked isUsingIntegratedVendor = false;
@tracked integratedVendorServiceType;
@tracked invalidReason;
@tracked metadataButtons = [
{
type: 'default',
text: 'Edit metadata',
icon: 'edit',
onClick: this.editMetaData,
},
];
@tracked uploadQueue = [];
acceptedFileTypes = [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/msword',
'application/pdf',
'application/x-pdf',
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'video/mp4',
'video/quicktime',
'video/x-msvideo',
'video/x-flv',
'video/x-ms-wmv',
'audio/mpeg',
'video/x-msvideo',
'application/zip',
'application/x-tar',
];
get renderableComponents() {
const renderableComponents = this.universe.getRenderableComponentsFromRegistry('fleet-ops:template:operations:orders:new');
return renderableComponents;
}
get renderableEntityInputComponents() {
const renderableComponents = this.universe.getRenderableComponentsFromRegistry('fleet-ops:template:operations:orders:new:entities-input');
return renderableComponents;
}
@not('isServicable') isNotServicable;
@alias('currentUser.latitude') userLatitude;
@alias('currentUser.longitude') userLongitude;
@alias('ordersController.leafletMap') leafletMap;
@equal('isCsvImportedOrder', false) isNotCsvImportedOrder;
@computed('isCustomFieldsValid', 'entities.length', 'isMultipleDropoffOrder', 'isFetchingQuotes', 'isSubscriptionValid', 'order.type', 'payload.{dropoff,pickup}', 'waypoints.length')
get isValid() {
const { isMultipleDropoffOrder, isSubscriptionValid, isFetchingQuotes } = this;
const isOrderTypeSet = isNotEmpty(this.order?.type);
const isWaypointsSet = this.waypoints?.length > 1;
const isPickupSet = isNotEmpty(this.payload?.pickup);
const isDropoffSet = isNotEmpty(this.payload?.dropoff);
// const isPayloadSet = this.entities?.length > 0;
if (isFetchingQuotes) {
return false;
}
if (!isSubscriptionValid) {
return false;
}
if (isMultipleDropoffOrder) {
return isOrderTypeSet && isWaypointsSet;
}
return isOrderTypeSet && isPickupSet && isDropoffSet && this.isCustomFieldsValid;
}
updatePayloadCoordinates() {
let waypoints = [];
let coordinates = [];
waypoints.pushObjects([this.payload.pickup, ...this.waypoints.map((waypoint) => waypoint.place), this.payload.dropoff]);
waypoints.forEach((place) => {
if (place && place.get('longitude') && place.get('latitude')) {
if (place.hasInvalidCoordinates) {
return;
}
coordinates.pushObject([place.get('longitude'), place.get('latitude')]);
}
});
this.payloadCoordinates = coordinates;
}
@computed('payloadCoordinates.length', 'waypoints.[]') get isServicable() {
return this.payloadCoordinates.length >= 2;
}
@computed('routePreviewArray.[]') get routePreviewCoordinates() {
// return this.routePreviewArray.filter((place) => place.get('hasValidCoordinates')).map((place) => place.get('latlng'));
return (
this.routePreviewArray
// .filter((place) => place.get('hasValidCoordinates'))
.map((place) => place.get('latlng'))
);
}
@computed('entities.[]', 'waypoints.[]') get entitiesByImportId() {
const groups = [];
// create groups
this.waypoints.forEach((waypoint) => {
const importId = waypoint.place._import_id ?? null;
if (importId) {
const entities = this.entities.filter((entity) => entity._import_id === importId);
const group = {
importId,
waypoint,
entities,
};
groups.pushObject(group);
}
});
return groups;
}
checkIfCustomFieldsValid() {
this.isCustomFieldsValid = this.customFields.every((customField) => {
if (!customField.required) {
return true;
}
const customFieldValue = this.customFieldValues[customField.id];
return customFieldValue && !isBlank(customFieldValue.value);
});
}
@action createOrder() {
if (!this.isValid) {
return;
}
this.previewRoute(false);
this.loader.showLoader('body', { loadingMessage: 'Creating Order...' });
const { order, groupedMetaFields, payload, entities, waypoints } = this;
const route = this.leafletOptimizedRoute ? this.getOptimizedRoute() : this.getRoute();
// set service quote if applicable
if (this.selectedServiceQuote) {
order.service_quote_uuid = this.selectedServiceQuote;
}
try {
order.serializeMeta().serializeMetaFromGroupedFields(groupedMetaFields).setPayload(payload).setRoute(route).get('payload').setWaypoints(waypoints).setEntities(entities);
} catch (error) {
this.notifications.serverError(error);
this.loader.removeLoader();
return;
}
// valiadate custom field inputs
for (let i = 0; i < this.customFields.length; i++) {
const customField = this.customFields[i];
if (customField.required) {
const customFieldValue = this.customFieldValues[customField.id];
if (!customFieldValue || isBlank(customFieldValue.value)) {
this.loader.removeLoader();
return this.notifications.error(this.intl.t('fleet-ops.operations.orders.index.new.input-field-required', { inputFieldName: customField.label }));
}
}
}
// create custom field values
for (let customFieldId in this.customFieldValues) {
const { value, value_type } = this.customFieldValues[customFieldId];
const customFieldValue = this.store.createRecord('custom-field-value', {
custom_field_uuid: customFieldId,
value,
value_type,
});
this.order.custom_field_values.push(customFieldValue);
}
// send event that fleetops is `creating` an order
this.universe.trigger('fleet-ops.order.creating', order);
this.isCreatingOrder = true;
try {
return order
.save()
.then((order) => {
// trigger event that fleet-ops created an order
this.universe.trigger('fleet-ops.order.created', order);
// transition to order view
return this.hostRouter.transitionTo(`console.fleet-ops.operations.orders.index.view`, order).then(() => {
this.notifications.success(this.intl.t('fleet-ops.operations.orders.index.new.success-message', { orderId: order.public_id }));
this.loader.removeLoader();
this.resetForm();
later(
this,
() => {
this.hostRouter.refresh();
},
100
);
});
})
.catch((error) => {
this.isCreatingOrder = false;
this.notifications.serverError(error);
this.loader.removeLoader();
});
} catch (error) {
this.notifications.error(error.message);
this.loader.removeLoader();
}
}
@action importOrder() {
const checkQueue = () => {
const uploadQueue = this.modalsManager.getOption('uploadQueue');
if (uploadQueue.length) {
this.modalsManager.setOption('acceptButtonDisabled', false);
} else {
this.modalsManager.setOption('acceptButtonDisabled', true);
}
};
this.modalsManager.show('modals/order-import', {
title: 'Import order(s) with spreadsheets',
acceptButtonText: 'Start Upload',
acceptButtonScheme: 'magic',
acceptButtonIcon: 'upload',
acceptButtonDisabled: true,
isProcessing: false,
uploadQueue: [],
fileQueueColumns: [
{ name: 'Type', valuePath: 'extension', key: 'type' },
{ name: 'File Name', valuePath: 'name', key: 'fileName' },
{ name: 'File Size', valuePath: 'size', key: 'fileSize' },
{ name: 'Upload Date', valuePath: 'file.lastModifiedDate', key: 'uploadDate' },
{ name: '', valuePath: '', key: 'delete' },
],
acceptedFileTypes: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/csv'],
queueFile: (file) => {
const uploadQueue = this.modalsManager.getOption('uploadQueue');
uploadQueue.pushObject(file);
checkQueue();
},
removeFile: (file) => {
const { queue } = file;
const uploadQueue = this.modalsManager.getOption('uploadQueue');
uploadQueue.removeObject(file);
queue.remove(file);
checkQueue();
},
confirm: async (modal) => {
const uploadQueue = this.modalsManager.getOption('uploadQueue');
const uploadedFiles = [];
const uploadTask = (file) => {
return new Promise((resolve) => {
this.fetch.uploadFile.perform(
file,
{
path: `uploads/fleet-ops/order-imports/${this.currentUser.companyId}`,
type: `order_import`,
},
(uploadedFile) => {
uploadedFiles.pushObject(uploadedFile);
resolve(uploadedFile);
}
);
});
};
if (!uploadQueue.length) {
return this.notifications.warning(this.intl.t('fleet-ops.operations.orders.index.new.warning-message'));
}
modal.startLoading();
modal.setOption('acceptButtonText', 'Uploading...');
for (let i = 0; i < uploadQueue.length; i++) {
const file = uploadQueue.objectAt(i);
await uploadTask(file);
}
this.modalsManager.setOption('acceptButtonText', 'Processing...');
this.modalsManager.setOption('isProcessing', true);
const files = uploadedFiles.map((file) => file.id);
let results;
try {
results = await this.fetch.post('orders/process-imports', { files });
} catch (error) {
return this.notifications.serverError(error);
}
const places = get(results, 'places');
const entities = get(results, 'entities');
if (isArray(places)) {
this.isMultipleDropoffOrder = true;
this.waypoints = places.map((_place) => {
const place = this.store.createRecord('place', _place);
return this.store.createRecord('waypoint', { place });
});
}
if (isArray(entities)) {
this.entities = entities.map((entity) => {
return this.store.createRecord('entity', entity);
});
}
this.notifications.success(this.intl.t('fleet-ops.operations.orders.index.new.import-success'));
this.isCsvImportedOrder = true;
this.previewDraftOrderRoute(this.payload, this.waypoints, this.isMultipleDropoffOrder);
modal.done();
},
decline: (modal) => {
this.modalsManager.setOption('uploadQueue', []);
modal.done();
},
});
}
@action async toggleAdhoc(on) {
const defaultDistanceInMeters = 5000;
if (on) {
const company = this.store.peekRecord('company', this.currentUser.companyId);
this.order.adhoc_distance = getWithDefault(company, 'options.fleetops.adhoc_distance', defaultDistanceInMeters);
} else {
this.order.adhoc_distance = defaultDistanceInMeters;
}
this.order.adhoc = on;
}
@action async toggleProofOfDelivery(on) {
this.order.pod_required = on;
if (on) {
this.order.pod_method = 'scan';
} else {
this.order.pod_method = null;
}
}
@action async checkServiceRates(shouldCheck) {
this.servicable = shouldCheck;
const params = {
coordinates: this.getCoordinatesFromPayload().join(';'),
};
let serviceRates = [];
if (this.isUsingIntegratedVendor) {
params.facilitator = this.order.facilitator.public_id;
}
// filter by order config type
if (this.orderConfig) {
params.service_type = this.orderConfig.key;
}
if (shouldCheck) {
try {
serviceRates = await this.fetch.get(`service-rates/for-route`, params);
serviceRates.unshiftObject({
service_name: 'Quote from all Service Rates',
id: 'all',
});
} catch (error) {
this.notifications.serverError(error);
}
this.serviceRates = serviceRates;
}
}
@action createPlace() {
const place = this.store.createRecord('place');
this.contextPanel.focus(place, 'editing', {
onAfterSave: () => {
this.contextPanel.clear();
},
});
}
@action editPlace(place) {
this.contextPanel.focus(place, 'editing', {
onAfterSave: () => {
this.contextPanel.clear();
},
});
}
@action async getQuotes(service) {
this.isFetchingQuotes = true;
let payload = this.payload.serialize();
let route = this.getRoute();
let distance = get(route, 'details.summary.totalDistance');
let time = get(route, 'details.summary.totalTime');
let service_type = this.order.type;
let scheduled_at = this.order.scheduled_at;
let facilitator = this.order.facilitator?.get('public_id');
let is_route_optimized = this.order.get('is_route_optimized');
let { waypoints, entities } = this;
let places = [];
if (this.payloadCoordinates?.length < 2) {
this.isFetchingQuotes = false;
return;
}
if (this.isUsingIntegratedVendor && this.integratedVendorServiceType) {
service_type = this.integratedVendorServiceType;
}
// get place instances from WaypointModel
for (let i = 0; i < waypoints.length; i++) {
let place = await waypoints[i].place;
places.pushObject(place);
}
setProperties(payload, { waypoints: places, entities });
if (!payload.type && this.order.type) {
setProperties(payload, { type: this.order.type });
}
this.fetch
.post('service-quotes/preliminary', {
payload: this._getSerializedPayload(payload),
distance,
time,
service,
service_type,
facilitator,
scheduled_at,
is_route_optimized,
})
.then((serviceQuotes) => {
set(this, 'serviceQuotes', isArray(serviceQuotes) ? serviceQuotes : []);
if (this.serviceQuotes.length && this.isUsingIntegratedVendor) {
set(this, 'selectedServiceQuote', this.serviceQuotes.firstObject?.uuid);
}
})
.catch(() => {
this.notifications.warning(this.intl.t('fleet-ops.operations.orders.index.new.service-warning'));
})
.finally(() => {
this.isFetchingQuotes = false;
});
}
_getSerializedPayload(payload) {
const serialized = {
pickup: this._seriailizeModel(payload.pickup),
dropoff: this._seriailizeModel(payload.dropoff),
entitities: this._serializeArray(payload.entities),
waypoints: this._serializeArray(payload.waypoints),
};
return serialized;
}
_seriailizeModel(model) {
if (isModel(model)) {
if (typeof model.toJSON === 'function') {
return model.toJSON();
}
if (typeof model.serialize === 'function') {
return model.serialize();
}
}
return model;
}
_serializeArray(array) {
return isArray(array) ? array.map((item) => this._seriailizeModel(item)) : array;
}
@action scheduleOrder(dateInstance) {
this.order.scheduled_at = dateInstance;
}
@action setupInterface() {
if (this.leafletMap && this.leafletMap.liveMap) {
this.leafletMap.liveMap.hideAll();
// track all layers added from this view
this.leafletMap.on('layeradd', ({ layer }) => {
// disable dragging of layer
if (layer.dragging && typeof layer.dragging.disable === 'function') {
layer.dragging.disable();
}
next(this, function () {
if (isArray(this.leafletLayers) && !this.leafletLayers.includes(layer)) {
this.leafletLayers.pushObject(layer);
}
});
});
} else {
// setup interface when livemap is ready
this.universe.on('fleet-ops.live-map.ready', () => {
this.setupInterface();
});
}
// switch to map mode
this.ordersController.setLayoutMode('map');
}
@action resetInterface() {
if (this.leafletMap && this.leafletMap.liveMap) {
this.leafletMap.liveMap.show(['drivers', 'vehicles', 'routes']);
}
}
@action getRoute() {
const details = this.leafletRoute;
const route = this.store.createRecord('route', { details });
return route;
}
@action getOptimizedRoute() {
const details = this.leafletOptimizedRoute;
const route = this.store.createRecord('route', { details });
return route;
}
@action setOptimizedRoute(route, trip, waypoints) {
let summary = { totalDistance: trip.distance, totalTime: trip.duration };
let payload = {
optimized: true,
coordinates: route,
waypoints,
trip,
summary,
};
this.leafletOptimizedRoute = payload;
}
@action removeRoutingControlPreview() {
const leafletMap = this.leafletMap;
const previewRouteControl = this.previewRouteControl;
let removed = false;
if (leafletMap && previewRouteControl instanceof RoutingControl) {
try {
previewRouteControl.remove();
removed = true;
} catch (e) {
// silent
}
if (!removed) {
try {
leafletMap.removeControl(previewRouteControl);
} catch (e) {
// silent
}
}
}
if (!removed) {
this.forceRemoveRoutePreview();
}
}
@action forceRemoveRoutePreview() {
const { leafletMap } = this;
leafletMap.eachLayer((layer) => {
if (layer instanceof L.Polyline || layer instanceof L.Marker) {
try {
layer.remove();
} catch (error) {
// silent error just continue with order processing if any
}
}
});
}
@action removePreviewRouteLayers() {
const { currentLeafletRoute, leafletMap } = this;
if (currentLeafletRoute) {
// target is the route, and waypoints is the markers
const { target, waypoints } = currentLeafletRoute;
leafletMap.removeLayer(target);
waypoints?.forEach((waypoint) => {
try {
leafletMap.removeLayer(waypoint);
} catch (error) {
// silent error just continue with order processing if any
}
});
}
}
@action clearLayers() {
if (this.leafletMap) {
try {
this.leafletMap.eachLayer((layer) => {
if (isArray(this.leafletLayers) && this.leafletLayers.includes(layer)) {
this.leafletMap.removeLayer(layer);
}
});
} catch (error) {
// fallback method with tracked layers
if (isArray(this.leafletLayers)) {
this.leafletLayers.forEach((layer) => {
try {
this.leafletMap.removeLayer(layer);
} catch (error) {
// silent error just continue with order processing if any
}
});
}
}
}
}
@action clearAllLayers() {
if (this.leafletMap) {
try {
this.leafletMap.eachLayer((layer) => {
this.leafletMap.removeLayer(layer);
});
} catch (error) {
// fallback method with tracked layers
if (isArray(this.leafletLayers)) {
this.leafletLayers.forEach((layer) => {
try {
this.leafletMap.removeLayer(layer);
} catch (error) {
// silent error just continue with order processing if any
}
});
}
}
}
}
@action createPlaceArrayFromPayload(payload, waypoints, isMultipleDropoffOrder = false) {
const routePreviewArray = [];
if (isMultipleDropoffOrder) {
for (let i = 0; i < waypoints.length; i++) {
if (waypoints[i].place) {
routePreviewArray.pushObject(waypoints[i].place);
}
}
} else {
if (payload.pickup) {
routePreviewArray.pushObject(payload.pickup);
}
if (payload.dropoff) {
routePreviewArray.pushObject(payload.dropoff);
}
}
return routePreviewArray;
}
@action createCoordinatesFromRoutePlaceArray(array) {
return array.filter((place) => place.get('hasValidCoordinates')).map((place) => place.get('latlng'));
}
@action previewDraftOrderRoute(payload, waypoints, isMultipleDropoffOrder = false) {
const leafletMap = this.leafletMap;
// if existing route preview on the map - remove it
this.removeRoutingControlPreview();
this.removeOptimizedRoute();
this.clearLayers();
if (!this.isRoutePreviewAnimationActive) {
this.previewRoute(true);
}
this.isViewingRoutePreview = true;
this.routePreviewArray = this.createPlaceArrayFromPayload(payload, waypoints, isMultipleDropoffOrder);
const canPreviewRoute = this.routePreviewArray.length > 0;
if (canPreviewRoute) {
const routingHost = getRoutingHost(payload, waypoints);
const router = new OSRMv1({
serviceUrl: `${routingHost}/route/v1`,
profile: 'driving',
});
this.previewRouteControl = new RoutingControl({
waypoints: this.routePreviewCoordinates,
alternativeClassName: 'hidden',
addWaypoints: false,
markerOptions: {
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],
}),
},
router,
}).addTo(leafletMap);
this.previewRouteControl.on('routesfound', (event) => {
const { routes } = event;
const leafletRoute = routes.firstObject;
this.currentLeafletRoute = event;
this.setProperties({ leafletRoute });
});
if (this.routePreviewCoordinates.length === 1) {
leafletMap.flyTo(this.routePreviewCoordinates[0], 18);
leafletMap.once('moveend', function () {
leafletMap.panBy([200, 0]);
});
} else {
leafletMap.flyToBounds(this.routePreviewCoordinates, {
paddingBottomRight: [300, 0],
maxZoom: this.routePreviewCoordinates.length === 2 ? 15 : 14,
animate: true,
});
leafletMap.once('moveend', function () {
leafletMap.panBy([150, 0]);
});
}
} else {
this.notifications.warning(this.intl.t('fleet-ops.operations.orders.index.new.no-route-warning'));
}
}
@action previewRoute(isViewingRoutePreview) {
this.isViewingRoutePreview = isViewingRoutePreview;
this.isRoutePreviewAnimationActive = isViewingRoutePreview;
if (isViewingRoutePreview === true) {
this.previewDraftOrderRoute(this.payload, this.waypoints, this.isMultipleDropoffOrder);
}
if (isViewingRoutePreview === false) {
this.removeRoutingControlPreview();
this.removeOptimizedRoute();
this.removePreviewRouteLayers();
this.clearLayers();
}
}
@action async optimizeRoute() {
this.isOptimizingRoute = true;
const leafletMap = this.leafletMap;
const coordinates = this.getCoordinatesFromPayload();
const routingHost = getRoutingHost(this.payload, this.waypoints);
const response = await this.fetch.routing(coordinates, { source: 'any', destination: 'any', annotations: true }, { host: routingHost }).catch(() => {
this.notifications.error(this.intl.t('fleet-ops.operations.orders.index.new.route-error'));
this.isOptimizingRoute = false;
});
this.isOptimizingRoute = false;
if (response && response.code === 'Ok') {
// remove current route display
this.removeRoutingControlPreview();
this.removeOptimizedRoute(leafletMap);
let trip = response.trips.firstObject;
let route = polyline.decode(trip.geometry);
let sortedWaypoints = [];
let optimizedRouteMarkers = [];
if (response.waypoints && isArray(response.waypoints)) {
const responseWaypoints = response.waypoints.sortBy('waypoint_index');
this.setOptimizedRoute(route, trip, responseWaypoints);
for (let i = 0; i < responseWaypoints.length; i++) {
const optimizedWaypoint = responseWaypoints.objectAt(i);
const optimizedWaypointLongitude = optimizedWaypoint.location.firstObject;
const optimizedWaypointLatitude = optimizedWaypoint.location.lastObject;
const waypointModel = findClosestWaypoint(optimizedWaypointLatitude, optimizedWaypointLongitude, this.waypoints);
// eslint-disable-next-line no-undef
// const optimizedWaypointMarker = new L.Marker(optimizedWaypoint.location.reverse()).addTo(leafletMap);
const [longitude, latitude] = getWithDefault(optimizedWaypoint.location, 'coordiantes', [0, 0]);
const optimizedWaypointMarker = new L.Marker([latitude, longitude]).addTo(leafletMap);
sortedWaypoints.pushObject(waypointModel);
optimizedRouteMarkers.pushObject(optimizedWaypointMarker);
}
this.waypoints = sortedWaypoints;
this.optimizedRouteMarkers = optimizedRouteMarkers;
}
// set order as route optimized
this.order.set('is_route_optimized', true);
// refetch quotes
if (this.isUsingIntegratedVendor) {
this.getQuotes();
}
// eslint-disable-next-line no-undef
let optimizedRoute = (this.optimizedRoutePolyline = new L.Polyline(route, { color: 'red' }).addTo(leafletMap));
// leafletMap.addLayer(optimizedRoute);
leafletMap.flyToBounds(optimizedRoute.getBounds(), {
paddingBottomRight: [0, 600],
animate: true,
maxZoom: 13,
});
} else {
this.notifications.error(this.intl.t('fleet-ops.operations.orders.index.new.route-error'));
this.isOptimizingRoute = false;
}
}
@action removeOptimizedRoute(_leafletMap = null) {
this.leafletOptimizedRoute = undefined;
const leafletMap = _leafletMap || this.leafletMap;
if (!leafletMap) {
return;
}
if (this.optimizedRoutePolyline) {
leafletMap.removeLayer(this.optimizedRoutePolyline);
}
for (let i = 0; i < this.optimizedRouteMarkers.length; i++) {
let marker = this.optimizedRouteMarkers.objectAt(i);
leafletMap.removeLayer(marker);
}
}
@action getCoordinatesFromPayload() {
this.notifyPropertyChange('payloadCoordinates');
return this.payloadCoordinates;
}
@action toggleMultiDropOrder(isMultipleDropoffOrder) {
this.isMultipleDropoffOrder = isMultipleDropoffOrder;
const { pickup, dropoff } = this.payload;
if (isMultipleDropoffOrder) {
if (pickup) {
this.addWaypoint({ place: pickup, customer: this.order.customer });
if (dropoff) {
this.addWaypoint({ place: dropoff, customer: this.order.customer });
}
// clear pickup and dropoff
this.payload.setProperties({ pickup: null, dropoff: null });
} else {
this.addWaypoint({ customer: this.order.customer });
}
} else {
const pickup = get(this.waypoints, '0.place');
const dropoff = get(this.waypoints, '1.place');
if (pickup) {
this.setPayloadPlace('pickup', pickup);
}
if (dropoff) {
this.setPayloadPlace('dropoff', dropoff);
}
this.clearWaypoints();
}
}
@action resetForm() {
const order = this.store.createRecord('order', { meta: [] });
const payload = this.store.createRecord('payload');
const driversQuery = {};
const meta = [];
const entities = [];
const waypoints = [];
const orderConfigs = [];
const orderConfig = undefined;
const isCreatingOrder = false;
const isMultipleDropoffOrder = false;
const leafletRoute = undefined;
const serviceRates = [];
const selectedServiceRate = undefined;
const selectedServiceQuote = undefined;
const servicable = false;
this.removeRoutingControlPreview();
this.removeOptimizedRoute();
this.setProperties({
order,
payload,
driversQuery,
meta,
entities,
waypoints,
orderConfigs,
orderConfig,
isCreatingOrder,
isMultipleDropoffOrder,
leafletRoute,
serviceRates,
selectedServiceQuote,
selectedServiceRate,
servicable,
});
this.resetInterface();
}
@action setConfig(event) {
const orderConfigId = event.target.value;
if (!orderConfigId) {
return;
}
const orderConfig = this.store.peekRecord('order-config', orderConfigId);
this.orderConfig = orderConfig;
this.order.set('order_config_uuid', orderConfig.id);
this.order.set('type', orderConfig.key);
// load custom fields
this.loadCustomFields.perform(orderConfig);
}
/**
* A task method to load custom fields from the store and group them.
* @task
*/
@task *loadCustomFields(orderConfig) {
this.customFieldGroups = yield this.store.query('category', { owner_uuid: orderConfig.id, for: 'custom_field_group' });
this.customFields = yield this.store.query('custom-field', { subject_uuid: orderConfig.id });
this.groupCustomFields();
this.checkIfCustomFieldsValid();
}
/**
* Organizes custom fields into their respective groups.
*/
groupCustomFields() {
for (let i = 0; i < this.customFieldGroups.length; i++) {
const group = this.customFieldGroups[i];
group.set(
'customFields',
this.customFields.filter((customField) => {
return customField.category_uuid === group.id;
})
);
}
}
@action setCustomFieldValue(value, customField) {
this.customFieldValues = {
...this.customFieldValues,
[customField.id]: {
value,
value_type: this._getCustomFieldValueType(customField),
},
};
this.checkIfCustomFieldsValid();
}
_getCustomFieldValueType(customField) {
if (customField.type === 'file-upload') {
return 'file';
}
if (customField.type === 'date-time-input') {
return 'date';
}
if (customField.type === 'model-select') {
return 'model';
}
return 'text';
}
@action setOrderFacilitator(model) {
this.order.set('facilitator', model);
// this.order.set('facilitator_type', `fleet-ops:${model.facilitator_type}`);
this.order.set('driver', null);
this.isUsingIntegratedVendor = model.isIntegratedVendor;
this.servicable = model.isIntegratedVendor;
if (model.service_types?.length) {
this.integratedVendorServiceType = model.service_types.firstObject.key;
}
if (model.isIntegratedVendor) {
this.getQuotes();
}
if (model) {
this.driversQuery = { facilitator: model.id };
}
}
@action setOrderCustomer(model) {
this.order.set('customer', model);
}
@action setWaypointCustomer(waypoint, model) {
waypoint.set('customer', model);
waypoint.set('customer_type', `fleet-ops:${model.customer_type}`);
}
@action selectIntegratedServiceType(key) {
this.integratedVendorServiceType = key;
if (this.isUsingIntegratedVendor) {
this.getQuotes();
}
}
@action async selectDriver(driver) {
this.order.set('driver_assigned', driver);
if (driver && driver.vehicle) {
const vehicle = await driver.vehicle;
this.order.set('vehicle_assigned', vehicle);
}
}
@action addCustomField() {
let label, value;
this.modalsManager.show('modals/meta-field-form', {
title: this.intl.t('fleet-ops.operations.orders.index.new.custom-field-title'),
acceptButtonIcon: 'check',
acceptButtonIconPrefix: 'fas',
acceptButtonText: 'Done',
declineButtonIcon: 'times',
declineButtonIconPrefix: 'fas',
label,
value,
confirm: (modal) => {
const label = modal.getOption('label');
const value = modal.getOption('value');
if (!label) {
return this.notifications.warning(this.intl.t('fleet-ops.operations.orders.index.new.label-warning'));
}
if (!value) {
return this.notifications.warning(this.intl.t('fleet-ops.operations.orders.index.new.value-warning'));
}
modal.startLoading();
this.order.meta.pushObject({
key: dasherize(label),
label,
value,
});
modal.done();
},
});
}
@action editCustomField(index) {
const metaField = this.order.meta.objectAt(index);
const { label, value } = metaField;
this.modalsManager.show('modals/meta-field-form', {
title: this.intl.t('fleet-ops.operations.orders.index.new.edit-field-title'),
acceptButtonIcon: 'save',
acceptButtonText: 'Save Changes',
label,
value,
confirm: (modal) => {
const label = modal.getOption('label');
const value = modal.getOption('value');
if (!label) {
return this.notifications.warning(this.intl.t('fleet-ops.operations.orders.index.new.label-warning'));
}
if (!value) {
return this.notifications.warning(this.intl.t('fleet-ops.operations.orders.index.new.value-warning'));
}
modal.startLoading();
this.order.meta.replace(index, 1, [
{
key: dasherize(label),
label,
value,
},
]);
modal.done();
},
});
}
@action editMetaData() {
let { meta } = this.order;
if (!isArray(meta)) {
meta = [];
}
this.modalsManager.show('modals/edit-meta-form', {
title: this.intl.t('fleet-ops.operations.orders.index.new.edit-metadata'),
hideDeclineButton: true,
acceptButtonIcon: 'check',
acceptButtonIconPrefix: 'fas',
acceptButtonText: 'Done',
meta,
addMetaField: (meta) => {
const label = 'New field';
meta.pushObject({
key: dasherize(label),
label,
value: null,
});
},
removeMetaField: (meta, index) => {
meta.removeAt(index);
},
confirm: (modal) => {
const meta = modal.getOption('meta');
this.order.meta = meta;
modal.done();
},
});
}
@action removeMeta(meta) {
this.meta.removeObject(meta);
}
@action setPayloadPlace(prop, place) {
this.payload[prop] = place;
// this.previewRoute(true);
this.previewDraftOrderRoute(this.payload, this.waypoints, this.isMultipleDropoffOrder);
if (this.isUsingIntegratedVendor) {
this.getQuotes();
}
this.updatePayloadCoordinates();
}
@action sortWaypoints({ sourceList, sourceIndex, targetList, targetIndex }) {
if (sourceList === targetList && sourceIndex === targetIndex) {
return;
}
const item = sourceList.objectAt(sourceIndex);
sourceList.removeAt(sourceIndex);
targetList.insertAt(targetIndex, item);
if (this.isViewingRoutePreview) {
this.previewDraftOrderRoute(this.payload, this.waypoints, this.isMultipleDropoffOrder);
}
}
@action addWaypoint(properties = {}) {
if (this.order.customer) {
properties.customer = this.order.customer;
}
const waypoint = this.store.createRecord('waypoint', properties);
this.waypoints.pushObject(waypoint);
this.updatePayloadCoordinates();
}
@action setWaypointPlace(index, place) {
if (!this.waypoints[index]) {
return;
}
this.waypoints[index].place = place;
if (this.waypoints.length) {
this.previewDraftOrderRoute(this.payload, this.waypoints, this.isMultipleDropoffOrder);
}
if (this.isUsingIntegratedVendor) {
this.getQuotes();
}
this.updatePayloadCoordinates();
}
@action removeWaypoint(waypoint) {
if (this.isMultipleDropoffOrder && this.waypoints.length === 1) {
return;
}
this.waypoints.removeObject(waypoint);
if (this.waypoints.length === 1) {
this.previewRoute(false);
} else {
this.previewDraftOrderRoute(this.payload, this.waypoints, this.isMultipleDropoffOrder);
}
this.updatePayloadCoordinates();
}
@action clearWaypoints() {
this.waypoints.clear();
if (this.isViewingRoutePreview) {
this.previewRoute(false);
}
}
@action setEntityDestionation(index, { target }) {
const { value } = target;
this.entities[index].destination_uuid = value;
}
@action addFromCustomEntity(customEntity) {
const entity = this.store.createRecord('entity', {
...customEntity,
id: undefined,
});
this.entities.pushObject(entity);
}
@action addEntities(entities = []) {
if (isArray(entities)) {
this.entities.pushObjects(entities);
}
}
@action addEntity(importId = null) {
const entity = this.store.createRecord('entity', {
_import_id: typeof importId === 'string' ? importId : null,
});
this.entities.pushObject(entity);
}
@action removeEntity(entity) {
if (this.entities.length === 1) {
return;
}
if (!entity.get('isNew')) {
return entity.destroyRecord();
}
this.entities.removeObject(entity);
}
@action editEntity(entity) {
this.modalsManager.show('modals/entity-form', {
title: this.intl.t('fleet-ops.operations.orders.index.new.edit-item'),
acceptButtonText: 'Save Changes',
entity,
uploadNewPhoto: (file) => {
const fileUrl = URL.createObjectURL(file.file);
if (entity.get('isNew')) {
const { queue } = file;
this.modalsManager.setOption('pendingFileUpload', file);
entity.set('photo_url', fileUrl);
queue.remove(file);
return;
} else {
entity.set('photo_url', fileUrl);
}
// Indicate loading
this.modalsManager.startLoading();
// Perform upload
return this.fetch.uploadFile.perform(
file,
{
path: `uploads/${this.currentUser.companyId}/entities/${entity.id}`,
subject_uuid: entity.id,
subject_type: 'fleet-ops:entity',
type: 'entity_photo',
},
(uploadedFile) => {
entity.setProperties({
photo_uuid: uploadedFile.id,
photo_url: uploadedFile.url,
photo: uploadedFile,
});
// Stop loading
this.modalsManager.stopLoading();
},
() => {
// Stop loading
this.modalsManager.stopLoading();
}
);
},
confirm: async (modal) => {
modal.startLoading();
const pendingFileUpload = modal.getOption('pendingFileUpload');
return entity.save().then(() => {
i