@ducna01120/fleetops-engine
Version:
Fleet & Transport Management Extension for Fleetbase
976 lines (857 loc) • 31 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 } from '@ember/object';
import { later } from '@ember/runloop';
import { not, notEmpty, alias } from '@ember/object/computed';
import { task } from 'ember-concurrency-decorators';
import { OSRMv1, Control as RoutingControl } from '@fleetbase/leaflet-routing-machine';
import getRoutingHost from '@fleetbase/ember-core/utils/get-routing-host';
export default class OperationsOrdersIndexViewController extends BaseController {
ordersController;
contactsController;
vendorsController;
driversController;
store;
modalsManager;
notifications;
intl;
currentUser;
fetch;
hostRouter;
socket;
universe;
contextPanel;
isLoadingAdditionalData = false;
isWaypointsCollapsed;
isEditingOrderNotes = false;
leafletRoute;
routeControl;
commentInput = '';
customFieldGroups = [];
customFields = [];
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',
];
userLatitude;
userLongitude;
detailPanelButtons = [
{
type: 'default',
text: 'Edit',
icon: 'pencil',
iconPrefix: 'fas',
permission: 'fleet-ops update order',
onClick: () => {
const order = this.model;
this.editOrder(order);
},
},
];
routePanelButtons = [
{
type: 'default',
text: 'Edit',
icon: 'pencil',
iconPrefix: 'fas',
permission: 'fleet-ops update-route-for order',
onClick: () => {
const order = this.model;
this.editOrderRoute(order);
},
},
];
notesPanelButtons = [
{
type: 'default',
text: 'Edit',
icon: 'pencil',
iconPrefix: 'fas',
permission: 'fleet-ops update order',
onClick: () => {
this.editOrderNotes();
},
},
];
waypointsIsNotCollapsed;
isMultiDropOrder;
leafletMap;
get renderableComponents() {
const renderableComponents = this.universe.getRenderableComponentsFromRegistry('fleet-ops:template:operations:orders:view');
return renderableComponents;
}
/** @var entitiesByDestination */
get entitiesByDestination() {
const groups = [];
// create groups
this.model.payload.waypoints.forEach((waypoint) => {
const destinationId = waypoint.id;
if (destinationId) {
const entities = this.model.payload.entities.filter((entity) => entity.destination_uuid === destinationId);
if (entities.length === 0) {
return;
}
const group = {
destinationId,
waypoint,
entities,
};
groups.pushObject(group);
}
});
return groups;
}
get orderWaypoints() {
const { payload } = this.model;
if (payload.waypoints && typeof payload.waypoints.toArray === 'function') {
return payload.waypoints.toArray();
}
return payload.waypoints;
}
get routeWaypoints() {
const { payload } = this.model;
let waypoints = [];
let 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;
}
*loadOrderRelations(order) {
yield order.loadTrackerData({}, { fromCache: true, expirationInterval: 10, expirationIntervalUnit: 'minute' });
yield order.loadETA();
yield order.loadOrderConfig();
yield order.loadPayload();
yield order.loadDriver();
yield order.loadTrackingNumber();
yield order.loadCustomer();
yield order.loadTrackingActivity();
yield order.loadPurchaseRate();
yield order.loadFiles();
this.loadCustomFields.perform(order);
}
/**
* A task method to load custom fields from the store and group them.
* @task
*/
*loadCustomFields(order) {
if (order.order_config_uuid) {
this.customFieldGroups = yield this.store.query('category', { owner_uuid: order.order_config_uuid, for: 'custom_field_group' });
this.customFields = yield this.store.query('custom-field', { subject_uuid: order.order_config_uuid });
this.groupCustomFields(order);
}
}
/**
* Organizes custom fields into their respective groups.
*/
groupCustomFields(order) {
// get custom fields and their values from order
const customFields = Array.from(this.customFields);
const customFieldValues = Array.from(order.custom_field_values);
// map values to the custom fields
const customFieldsWithValues = customFields.map((customField) => {
customField.value = customFieldValues.find((customFieldValue) => customFieldValue.custom_field_uuid === customField.id);
return customField;
});
// update custom fields with values
this.customFields = customFieldsWithValues;
// group and update custom fields
this.customFieldGroups = this.customFieldGroups.map((group) => {
group.set(
'customFields',
customFieldsWithValues.filter((customField) => {
return customField.category_uuid === group.id;
})
);
return group;
});
}
resetView() {
this.removeRoutingControlPreview();
this.resetInterface();
}
resetInterface() {
const liveMap = this.leafletMap ? this.leafletMap.liveMap : null;
if (liveMap) {
liveMap.reload();
liveMap.showAll();
}
}
removeRoutingControlPreview() {
const { leafletMap, routeControl } = this;
if (routeControl instanceof RoutingControl) {
try {
routeControl.remove();
} catch (e) {
// silent
}
}
if (leafletMap instanceof L.Map) {
try {
leafletMap.removeControl(routeControl);
} catch (e) {
// silent
}
}
this.forceRemoveRoutePreview();
}
forceRemoveRoutePreview() {
const { leafletMap } = this;
if (leafletMap instanceof L.Map) {
leafletMap.eachLayer((layer) => {
if (layer instanceof L.Polyline || layer instanceof L.Marker) {
layer.remove();
}
});
}
}
setupInterface() {
// always set map layout
this.ordersController.setLayoutMode('map');
// create initial setup function which runs 600ms after invoked
const setup = (ms = 600) => {
return later(
this,
() => {
const liveMap = this.leafletMap ? this.leafletMap.liveMap : null;
if (liveMap) {
liveMap.drivers = [];
liveMap.vehicles = [];
liveMap.places = [];
}
this.displayOrderRoute();
this.displayOrderDriverAssigned();
},
ms
);
};
// create a display order route only function
const displayOrderRoute = () => {
return later(
this,
() => {
return this.displayOrderRoute();
},
300
);
};
// re-display order routes when livemap has coordinates
this.universe.on('fleet-ops.live-map.has_coordinates', this, displayOrderRoute);
// when transitioning away kill event listener
this.hostRouter.on('routeWillChange', () => {
const isListening = this.universe.has('fleet-ops.live-map.has_coordinates');
if (isListening) {
this.universe.off('fleet-ops.live-map.has_coordinates', this, displayOrderRoute);
}
});
// run setup
setup();
}
getPayloadCoordinates(payload) {
let waypoints = [];
let 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;
}
getPayloadWaypointsAsArray() {
if (this.model.payload && this.model.payload.waypoints) {
return this.model.payload.waypoints.toArray();
}
return [];
}
displayOrderRoute() {
const leafletMap = this.leafletMap;
const payload = this.model.payload;
const waypoints = this.getPayloadCoordinates(payload);
const routingHost = getRoutingHost(payload, this.getPayloadWaypointsAsArray());
if (!waypoints || waypoints.length < 2 || !leafletMap) {
return;
}
// center on first coordinate
leafletMap.stop();
leafletMap.flyTo(waypoints.firstObject);
const router = new OSRMv1({
serviceUrl: `${routingHost}/route/v1`,
profile: 'driving',
});
this.routeControl = new RoutingControl({
waypoints,
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],
}),
},
alternativeClassName: 'hidden',
addWaypoints: false,
router,
}).addTo(leafletMap);
this.routeControl.on('routesfound', (event) => {
const { routes } = event;
this.leafletRoute = routes.firstObject;
});
later(
this,
() => {
leafletMap.flyToBounds(waypoints, {
paddingBottomRight: [200, 0],
maxZoom: 14,
animate: true,
});
leafletMap.once('moveend', function () {
leafletMap.panBy([250, 0]);
});
},
300
);
}
displayOrderDriverAssigned() {
const driverAssigned = this.model.driver_assigned;
if (driverAssigned) {
this.leafletMap.liveMap.drivers = [driverAssigned];
this.leafletMap.liveMap.show('drivers');
}
}
/**
* Edit order details.
*
* @param {OrderModel} order
* @param {Object} options
* @void
*/
toggleWaypointsCollapse() {
const _isWaypointsCollapsed = this.isWaypointsCollapsed;
this.isWaypointsCollapsed = !_isWaypointsCollapsed;
}
/**
* Edit order details.
*
* @param {OrderModel} order
* @param {Object} options
* @void
*/
editOrder(order, options = {}) {
options = options === null ? {} : options;
this.modalsManager.show('modals/order-form', {
title: this.intl.t('fleet-ops.operations.orders.index.view.edit-order-title'),
acceptButtonText: 'Save Changes',
acceptButtonIcon: 'save',
setOrderFacilitator: (model) => {
order.set('facilitator', model);
order.set('facilitator_type', `fleet-ops:${model.facilitator_type}`);
order.set('driver', null);
if (model) {
this.modalsManager.setOptions('driversQuery', {
facilitator: model.id,
});
}
},
setOrderCustomer: (model) => {
order.set('customer', model);
order.set('customer_type', `fleet-ops:${model.customer_type}`);
},
setDriver: (driver) => {
order.set('driver_assigned', driver);
if (driver && driver.vehicle) {
order.set('vehicle_assigned', driver.vehicle);
}
if (!driver) {
order.set('driver_assigned_uuid', null);
}
},
setVehicle: (vehicle) => {
order.set('vehicle_assigned', vehicle);
if (!vehicle) {
order.set('vehicle_assigned_uuid', null);
}
},
scheduleOrder: (dateInstance) => {
order.scheduled_at = dateInstance;
},
driversQuery: {},
order,
confirm: async (modal) => {
modal.startLoading();
try {
await order.save();
this.notifications.success(options.successNotification || this.intl.t('fleet-ops.operations.orders.index.view.update-success', { orderId: order.public_id }));
modal.done();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
decline: () => {
order.payload.rollbackAttributes();
this.modalsManager.done();
},
...options,
});
}
/**
* View order RAW order meta.
*
* @param {OrderModel} order
* @void
*/
viewOrderMeta(order) {
this.modalsManager.show('modals/order-meta', {
title: this.intl.t('fleet-ops.operations.orders.index.view.order-metadata'),
acceptButtonText: 'Done',
acceptButtonIcon: 'check',
acceptButtonIconPrefix: 'fas',
hideDeclineButton: true,
order,
});
}
editOrderNotes() {
this.isEditingOrderNotes = true;
}
cancelEditOrderNotes() {
this.isEditingOrderNotes = false;
}
*saveOrderNotes() {
const { notes } = this.model;
try {
yield this.model.persistProperty('notes', notes);
this.notifications.success(this.intl.t('fleet-ops.operations.orders.index.view.order-notes-updated'));
this.isEditingOrderNotes = false;
} catch (error) {
this.notifications.serverError(error);
}
}
/**
* Prompt to unassign driver from otder.
*
* @param {OrderModel} order
* @param {*} [options={}]
* @memberof OperationsOrdersIndexViewController
*/
unassignDriver(order, options = {}) {
this.modalsManager.confirm({
title: this.intl.t('fleet-ops.operations.orders.index.view.edit-order-title', { driverName: order.driver_assigned.name }),
body: this.intl.t('fleet-ops.operations.orders.index.view.unassign-body'),
order,
confirm: async (modal) => {
modal.startLoading();
order.setProperties({
driver_assigned: null,
driver_assigned_uuid: null,
});
try {
await order.save();
this.notifications.success(this.intl.t('fleet-ops.operations.orders.index.view.unassign-success'));
modal.done();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
...options,
});
}
/**
* Edit order routing details.
*
* @param {OrderModel} order
* @param {Object} options
* @void
*/
async editOrderRoute(order, options = {}) {
const updateRouteDisplay = () => {
later(
this,
() => {
this.displayOrderRoute();
},
100
);
};
this.contextPanel.focus(order, 'editingRoute', {
args: {
isResizable: true,
latitude: this.userLatitude,
longitude: this.userLongitude,
...options,
},
onRouteChanged: () => {
updateRouteDisplay();
},
onAfterSave: () => {
this.contextPanel.clear();
updateRouteDisplay();
},
});
}
/**
* Cancel the currently viewing order
*
* @param {OrderModel} order
* @void
*/
cancelOrder(order) {
this.ordersController.cancelOrder(order, {
onConfirm: () => {
order.loadTrackingActivity();
},
});
}
/**
* Delete the currently viewing order
*
* @param {OrderModel} order
* @void
*/
deleteOrder(order) {
this.ordersController.deleteOrder(order, {
onConfirm: () => {
return this.transitionBack();
},
});
}
/**
* Sends the order for dispatch
*
* @param {OrderModel} order
* @void
*/
dispatchOrder(order) {
this.ordersController.dispatchOrder(order, {
onConfirm: () => {
this.loadOrderRelations.perform(order);
},
});
}
/**
* Sends user to this orders socket channel
*
* @param {OrderModel} order
* @void
*/
listenToSocket(order) {
this.hostRouter.transitionTo('console.developers.sockets.view', `order.${order.public_id}`);
}
/**
* Prompt user to update order activity
*
* @param {OrderModel} order
* @void
*/
async createNewActivity(order) {
this.modalsManager.displayLoader();
const activityOptions = await this.fetch.get(`orders/next-activity/${order.id}`);
await this.modalsManager.done();
this.modalsManager.show(`modals/order-new-activity`, {
title: this.intl.t('fleet-ops.operations.orders.index.view.add-activity-title'),
acceptButton: false,
selected: null,
custom: {
status: '',
details: '',
code: '',
},
order,
activityOptions,
confirm: async (modal) => {
modal.startLoading();
let { selected, custom } = modal.getOptions(['custom', 'selected']);
let activity = selected !== 'custom' ? activityOptions[selected] : null;
if (selected === 'custom') {
if (!custom.status || !custom.details || !custom.code) {
modal.stopLoading();
return this.notifications.warning(this.intl.t('fleet-ops.operations.orders.index.view.invalid-warning'));
}
activity = custom;
}
try {
await this.fetch.patch(`orders/update-activity/${order.id}`, { activity });
modal.stopLoading();
return later(
this,
() => {
return this.hostRouter.refresh();
},
100
);
} catch (error) {
modal.stopLoading();
this.notifications.serverError(error);
}
},
});
}
/**
* Prompt user to assign a driver
*
* @param {OrderModel} order
* @void
*/
async assignDriver(order) {
if (order.canLoadDriver) {
this.modalsManager.displayLoader();
order.driver = await this.store.findRecord('driver', order.driver_uuid);
await this.modalsManager.done();
}
this.modalsManager.show(`modals/order-assign-driver`, {
title: order.driver_uuid ? this.intl.t('fleet-ops.operations.orders.index.view.change-order') : this.intl.t('fleet-ops.operations.orders.index.view.assign-order'),
acceptButtonText: 'Save Changes',
order,
confirm: async (modal) => {
modal.startLoading();
try {
await order.save();
this.notifications.success(this.intl.t('fleet-ops.operations.orders.index.view.assign-success', { orderId: order.public_id }));
modal.done();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
}
/**
* View order label
*
* @param {OrderModel} order
* @void
*/
async viewOrderLabel(order) {
// render dialog to display label within
this.modalsManager.show(`modals/order-label`, {
title: 'Order Label',
modalClass: 'modal-xl',
acceptButtonText: 'Done',
hideDeclineButton: true,
order,
});
// load the pdf label from base64
// eslint-disable-next-line no-undef
const fileReader = new FileReader();
const pdfStream = await this.fetch.get(`orders/label/${order.public_id}?format=base64`).then((resp) => resp.data);
// eslint-disable-next-line no-undef
const base64 = await fetch(`data:application/pdf;base64,${pdfStream}`);
const blob = await base64.blob();
// load into file reader
fileReader.onload = (event) => {
const data = event.target.result;
this.modalsManager.setOption('data', data);
};
fileReader.readAsDataURL(blob);
}
/**
* View order label
*
* @param {WaypointModel} waypoint
* @void
*/
async viewWaypointLabel(waypoint, dd) {
if (dd && typeof dd.actions.close === 'function') {
dd.actions.close();
}
// render dialog to display label within
this.modalsManager.show(`modals/order-label`, {
title: 'Waypoint Label',
modalClass: 'modal-xl',
acceptButtonText: 'Done',
hideDeclineButton: true,
});
// load the pdf label from base64
// eslint-disable-next-line no-undef
const fileReader = new FileReader();
const pdfStream = await this.fetch.get(`orders/label/${waypoint.waypoint_public_id}?format=base64`).then((resp) => resp.data);
// eslint-disable-next-line no-undef
const base64 = await fetch(`data:application/pdf;base64,${pdfStream}`);
const blob = await base64.blob();
// load into file reader
fileReader.onload = (event) => {
const data = event.target.result;
this.modalsManager.setOption('data', data);
};
fileReader.readAsDataURL(blob);
}
/**
* Reloads tracking activity for this order.
*
* @void
*/
reloadTrackingStatuses() {
return this.model.loadTrackingActivity();
}
/**
* Uses router service to transition back to `orders.index`
*
* @void
*/
transitionBack() {
return this.transitionToRoute('operations.orders.index');
}
async viewCustomer({ customer, customer_is_contact }) {
if (customer_is_contact) {
this.contactsController.viewContact(customer);
return;
}
this.vendorsController.viewVendor(customer);
}
focusOrderAssignedDriver({ driver_assigned, driver_assigned_uuid, canLoadDriver }) {
// if can load the driver then load and display via context
if (canLoadDriver) {
return this.store.findRecord('driver', driver_assigned_uuid).then((driver) => {
this.contextPanel.focus(driver);
});
}
// if driver already loaded use this
if (driver_assigned) {
this.contextPanel.focus(driver_assigned);
}
}
async viewFacilitator({ facilitator, facilitator_is_contact }) {
if (facilitator_is_contact) {
this.contactsController.viewContact(facilitator);
return;
}
this.vendorsController.viewVendor(facilitator);
}
addEntity(destination = null) {
const entity = this.store.createRecord('entity', {
payload_uuid: this.model.payload.id,
destination_uuid: destination ? destination.id : null,
});
this.model.payload.entities.pushObject(entity);
}
removeEntity(entity) {
entity.destroyRecord();
}
editEntity(entity) {
this.modalsManager.show('modals/entity-form', {
title: 'Edit Item',
acceptButtonText: 'Save Changes',
entity,
uploadNewPhoto: (file) => {
const fileUrl = URL.createObjectURL(file.blob);
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');
try {
await entity.save();
if (pendingFileUpload) {
return modal.invoke('uploadNewPhoto', pendingFileUpload);
}
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
}
queueFile(file) {
// since we have dropzone and upload button within dropzone validate the file state first
// as this method can be called twice from both functions
if (['queued', 'failed', 'timed_out', 'aborted'].indexOf(file.state) === -1) {
return;
}
// Queue and upload immediatley
this.uploadQueue.pushObject(file);
this.fetch.uploadFile.perform(
file,
{
path: 'uploads/fleet-ops/order-files',
subject_uuid: this.model.id,
subject_type: 'fleet-ops:order',
type: 'order_file',
},
(uploadedFile) => {
this.model.files.pushObject(uploadedFile);
this.uploadQueue.removeObject(file);
},
() => {
this.uploadQueue.removeObject(file);
// remove file from queue
if (file.queue && typeof file.queue.remove === 'function') {
file.queue.remove(file);
}
}
);
}
removeFile(file) {
return file.destroyRecord();
}
removeCustomFieldFile(customFieldValue, file) {
customFieldValue.set('value', null);
customFieldValue.save().then(() => {
return file.destroyRecord();
});
}
}