@ducna01120/fleetops-engine
Version:
Fleet & Transport Management Extension for Fleetbase
1,045 lines (957 loc) • 29.5 kB
JavaScript
import BaseController from '@ducna01120/fleetops-engine/controllers/base-controller';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { equal } from '@ember/object/computed';
import { isArray } from '@ember/array';
import { isBlank } from '@ember/utils';
import { timeout } from 'ember-concurrency';
import { task } from 'ember-concurrency-decorators';
import fromStore from '@fleetbase/ember-core/decorators/from-store';
export default class OperationsOrdersIndexController extends BaseController {
currentUser;
fetch;
intl;
filters;
hostRouter;
notifications;
modalsManager;
crud;
universe;
socket;
abilities;
theme;
/**
* Queryable parameters for this controller's model
*
* @var {Array}
*/
queryParams = [
'page',
'limit',
'sort',
'query',
'public_id',
'internal_id',
'payload',
'tracking_number',
'facilitator',
'customer',
'driver',
'vehicle',
'pickup',
'dropoff',
'created_by',
'updated_by',
'status',
'type',
'layout',
'drawerOpen',
'drawerTab',
'orderPanelOpen',
];
/**
* The current driver being focused.
*
* @var {DriverModel|null}
*/
focusedDriver;
/**
* The current vehicle being focused.
*
* @var {VehicleModel|null}
*/
focusedVehicle;
/**
* The current page of data being viewed
*
* @var {Integer}
*/
page = 1;
/**
* The maximum number of items to show per page
*
* @var {Integer}
*/
limit;
/**
* The param to sort the data on, the param with prepended `-` is descending
*
* @var {String}
*/
sort = '-created_at';
/**
* The filterable param `public_id`
*
* @var {String}
*/
public_id;
/**
* The filterable param `internal_id`
*
* @var {String}
*/
internal_id;
/**
* The filterable param `tracking`
*
* @var {String}
*/
tracking;
/**
* The filterable param `facilitator`
*
* @var {String}
*/
facilitator;
/**
* The filterable param `customer`
*
* @var {String}
*/
customer;
/**
* The filterable param `driver`
*
* @var {String}
*/
driver;
/**
* The filterable param `vehicle`
*
* @var {String}
*/
vehicle;
/**
* The filterable param `payload`
*
* @var {String}
*/
payload;
/**
* The filterable param `pickup`
*
* @var {String}
*/
pickup;
/**
* The filterable param `dropoff`
*
* @var {String}
*/
dropoff;
/**
* The filterable param `updated_by`
*
* @var {String}
*/
updated_by;
/**
* The filterable param `created_by`
*
* @var {String}
*/
created_by;
/**
* The filterable param `status`
*
* @var {String}
*/
status;
/**
* The filterable param `type` - Filter by order type
*
* @var {String}
*/
type;
/**
* Flag to determine if the search is visible
*
* @type {Boolean}
*/
isSearchVisible = false;
/**
* Flag to determine if the orders panel is visible
*
* @type {Boolean}
*/
isOrdersPanelVisible = false;
/**
* Count of active orders
*
* @type {Number}
*/
activeOrdersCount = 0;
/**
* The context for the order list overlay panel.
*
* @type {Object}
*/
orderListOverlayContext;
/**
* Reference to the leaflet map object
*
* @type {Object}
*/
leafletMap;
/**
* Reference to the drawer context API.
*
* @type {Object}
*/
drawer;
/**
* Current layout type (e.g., 'map', 'table', 'kanban', 'analytics')
*
* @type {String}
*/
layout = 'map';
/**
* Decides if scope drawer is open.
*
* @type {Boolean}
*/
drawerOpen = 0;
/**
* The drawer tab that is active.
*
* @type {Boolean}
*/
drawerTab;
/**
* Filterable status options for orders.
*
* @type {Array}
*/
statusOptions = [];
/**
* Flag to determine if the layout is 'map'
*
* @type {Boolean}
*/
isMapLayout;
/**
* Flag to determine if the layout is 'table'
*
* @type {Boolean}
*/
isTableLayout;
/**
* Flag to determine if the view is 'kanban'
*
* @type {Boolean}
*/
isKanbanView;
/**
* Flag to determine if the layout is 'analytics'
*
* @type {Boolean}
*/
isAnalyticsLayout;
/**
* All available order configs.
*
* @memberof OperationsOrdersIndexController
*/
orderConfigs;
/**
* All columns applicable for orders
*
* @var {Array}
*/
columns = [
{
label: this.intl.t('fleet-ops.common.id'),
valuePath: 'public_id',
width: '140px',
cellComponent: 'table/cell/link-to',
route: 'operations.orders.index.view',
onLinkClick: this.viewOrder,
permission: 'fleet-ops view order',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/string',
},
{
label: this.intl.t('fleet-ops.common.internal-id'),
valuePath: 'internal_id',
cellComponent: 'click-to-copy',
width: '125px',
hidden: true,
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/string',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.payload'),
valuePath: 'payload.public_id',
cellComponent: 'click-to-copy',
resizable: true,
hidden: true,
width: '125px',
filterable: true,
filterLabel: 'Payload ID',
filterParam: 'payload',
filterComponent: 'filter/string',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.driver-assigned'),
cellComponent: 'cell/driver-name',
valuePath: 'driver_assigned',
modelPath: 'driver_assigned',
width: '210px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/model',
filterComponentPlaceholder: 'Select driver for order',
filterParam: 'driver',
model: 'driver',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.pickup'),
valuePath: 'pickupName',
cellComponent: 'table/cell/base',
width: '160px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/model',
filterOptionLabel: 'address',
filterComponentPlaceholder: 'Select order pickup location',
filterParam: 'pickup',
modelNamePath: 'address',
model: 'place',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.dropoff'),
valuePath: 'dropoffName',
cellComponent: 'table/cell/base',
width: '160px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/model',
filterComponentPlaceholder: 'Select order dropoff location',
filterParam: 'dropoff',
modelNamePath: 'address',
model: 'place',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.customer'),
valuePath: 'customer.name',
cellComponent: 'table/cell/base',
width: '125px',
resizable: true,
sortable: true,
hidden: false,
filterable: true,
filterComponent: 'filter/model',
filterComponentPlaceholder: 'Select order customer',
filterParam: 'customer',
model: 'customer',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.vehicle-assigned'),
cellComponent: 'cell/vehicle-name',
valuePath: 'vehicle_assigned.display_name',
modelPath: 'vehicle_assigned',
showOnlineIndicator: true,
width: '170px',
hidden: true,
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/model',
filterComponentPlaceholder: 'Select vehicle for order',
filterParam: 'vehicle',
model: 'vehicle',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.facilitator'),
valuePath: 'facilitator.name',
cellComponent: 'table/cell/base',
width: '125px',
resizable: true,
hidden: true,
sortable: true,
filterable: true,
filterComponent: 'filter/model',
filterComponentPlaceholder: 'Select order facilitator',
filterParam: 'facilitator',
model: 'vendor',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.scheduled-at'),
valuePath: 'scheduledAt',
sortParam: 'scheduled_at',
filterParam: 'scheduled_at',
width: '150px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/date',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.items'),
cellComponent: 'table/cell/base',
valuePath: 'item_count',
resizable: true,
hidden: true,
width: '50px',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.transaction'),
cellComponent: 'table/cell/base',
valuePath: 'transaction_amount',
width: '50px',
resizable: true,
hidden: true,
sortable: true,
},
{
label: this.intl.t('fleet-ops.operations.orders.index.tracking'),
valuePath: 'tracking_number.tracking_number',
cellComponent: 'click-to-copy',
width: '170px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/string',
},
{
label: this.intl.t('fleet-ops.common.type'),
valuePath: 'type',
width: '100px',
resizable: true,
hidden: true,
sortable: true,
filterable: true,
filterComponent: 'filter/select',
filterOptions: this.orderConfigs,
filterOptionLabel: 'name',
filterOptionValue: 'id',
filterComponentPlaceholder: 'Filter by order config',
},
{
label: this.intl.t('fleet-ops.common.status'),
valuePath: 'status',
cellComponent: 'table/cell/status',
width: '120px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/multi-option',
filterOptions: this.statusOptions,
},
{
label: this.intl.t('fleet-ops.common.created-at'),
valuePath: 'createdAtShort',
sortParam: 'created_at',
filterParam: 'created_at',
width: '140px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/date',
},
{
label: this.intl.t('fleet-ops.common.updated-at'),
valuePath: 'updatedAtShort',
sortParam: 'updated_at',
filterParam: 'updated_at',
width: '125px',
resizable: true,
sortable: true,
hidden: true,
filterable: true,
filterComponent: 'filter/date',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.created-by'),
valuePath: 'created_by_name',
width: '125px',
resizable: true,
hidden: true,
filterable: true,
filterComponent: 'filter/model',
filterComponentPlaceholder: 'Select user',
filterParam: 'created_by',
model: 'user',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.updated-by'),
valuePath: 'updated_by_name',
width: '125px',
resizable: true,
hidden: true,
filterable: true,
filterComponent: 'filter/model',
filterComponentPlaceholder: 'Select user',
filterParam: 'updated_by',
model: 'user',
},
{
label: '',
cellComponent: 'table/cell/dropdown',
ddButtonText: false,
ddButtonIcon: 'ellipsis-h',
ddButtonIconPrefix: 'fas',
ddMenuLabel: 'Order Actions',
cellClassNames: 'overflow-visible',
wrapperClass: 'flex items-center justify-end mx-2',
width: '12%',
actions: [
{
label: this.intl.t('fleet-ops.operations.orders.index.view-order'),
icon: 'eye',
fn: this.viewOrder,
permission: 'fleet-ops view order',
},
{
label: this.intl.t('fleet-ops.operations.orders.index.dispatch-order'),
icon: 'paper-plane',
fn: this.dispatchOrder,
permission: 'fleet-ops dispatch order',
isVisible: (order) => order.canBeDispatched,
},
{
label: this.intl.t('fleet-ops.operations.orders.index.cancel-order'),
icon: 'ban',
fn: this.cancelOrder,
permission: 'fleet-ops cancel order',
},
{
separator: true,
},
{
label: this.intl.t('fleet-ops.operations.orders.index.delete-order'),
icon: 'trash',
fn: this.deleteOrder,
permission: 'fleet-ops delete order',
},
],
sortable: false,
filterable: false,
resizable: false,
searchable: false,
},
];
/**
* Creates an instance of OperationsOrdersIndexController.
* @memberof OperationsOrdersIndexController
*/
constructor() {
super(...arguments);
this.listenForOrderEvents();
this.getOrderStatusOptions.perform();
}
*getOrderStatusOptions() {
try {
this.statusOptions = yield this.fetch.get('orders/statuses');
} catch (error) {
this.notifications.serverError(error);
}
}
/**
* Listen for incoming order events to refresh listing.
*
* @memberof OperationsOrdersIndexController
*/
async listenForOrderEvents() {
// wait for user to be loaded into service
this.currentUser.on('user.loaded', () => {
// Get socket instance
const socket = this.socket.instance();
// The channel ID to listen on
const channelId = `company.${this.currentUser.companyId}`;
// Listed on company channel
const channel = socket.subscribe(channelId);
// Events which should trigger refresh
const listening = ['order.ready', 'order.driver_assigned'];
// Listen for channel subscription
(async () => {
for await (let output of channel) {
const { event } = output;
// if an order event refresh orders
if (typeof event === 'string' && listening.includes(event)) {
this.hostRouter.refresh();
}
}
})();
// disconnect when transitioning
this.hostRouter.on('routeWillChange', () => {
channel.close();
});
});
}
/**
* The search task.
*
* @void
*/
*search({ target: { value } }) {
// if no query don't search
if (isBlank(value)) {
this.query = null;
return;
}
// timeout for typing
yield timeout(250);
// reset page for results
if (this.page > 1) {
this.page = 1;
}
// update the query param
this.query = value;
}
/**
* Reload layout view.
*/
reload() {
return this.hostRouter.refresh();
}
/**
* Hides all elements on the live map.
* @action
* @memberof OperationsOrdersIndexController
*/
resetView() {
if (this.leafletMap && this.leafletMap.liveMap) {
this.leafletMap.liveMap.hideAll();
}
}
/**
* Toggles the visibility of the search interface.
* @action
* @memberof OperationsOrdersIndexController
*/
toggleSearch() {
this.isSearchVisible = !this.isSearchVisible;
}
/**
* Set the order list overlay context.
* @action
* @memberof OperationsOrdersIndexController
*/
setOrderListOverlayContext(orderListOverlayContext) {
this.orderListOverlayContext = orderListOverlayContext;
}
/**
* Toggles the visibility of the orders panel.
* @action
* @memberof OperationsOrdersIndexController
*/
toggleOrdersPanel() {
if (this.orderListOverlayContext) {
this.orderListOverlayContext.toggle();
}
}
/**
* Hides the orders panel.
* @action
* @memberof OperationsOrdersIndexController
*/
hideOrdersPanel() {
if (this.orderListOverlayContext) {
this.orderListOverlayContext.close();
}
}
/**
* Shows the orders panel.
* @action
* @memberof OperationsOrdersIndexController
*/
showOrdersPanel() {
if (this.orderListOverlayContext) {
this.orderListOverlayContext.open();
}
}
/**
* Zooms the map in or out.
* @param {string} [direction='in'] - The direction to zoom. Either 'in' or 'out'.
* @action
* @memberof OperationsOrdersIndexController
*/
zoomMap(direction = 'in') {
if (direction === 'in') {
this.leafletMap?.zoomIn();
} else {
this.leafletMap?.zoomOut();
}
}
/**
* Sets the layout mode and triggers a layout change event.
* @param {string} mode - The layout mode to set. E.g., 'table'.
* @action
* @memberof OperationsOrdersIndexController
*/
setLayoutMode(mode) {
this.layout = mode;
if (mode === 'table') {
this.isSearchVisible = false;
}
this.universe.trigger('fleet-ops.dashboard.layout.changed', mode);
}
/**
* Sets the map references for this component.
* Extracts the `liveMap` from the `target` object passed in the event and sets it as `this.liveMap`.
* Also, sets `target` as `this.leafletMap`.
*
* @param {Object} event - The event object containing the map references.
* @param {Object} event.target - The target map object.
* @param {Object} event.target.liveMap - The live map reference.
* @action
* @memberof OperationsOrdersIndexController
*/
setMapReference({ target }) {
this.leafletMap = target;
this.liveMap = target.liveMap;
}
previewOrderRoute(order) {
if (this.liveMap) {
this.liveMap.previewOrderRoute(order);
}
}
restoreDefaultLiveMap() {
if (this.liveMap) {
this.liveMap.restoreDefaultLiveMap();
}
}
/**
* Sets the drawer component context api.
*
* @param {Object} drawerApi
* @memberof OperationsOrdersIndexController
*/
setDrawerContext(drawerApi) {
this.drawer = drawerApi;
}
/**
* Toggles the LiveMap drawer component.
*
* @memberof OperationsOrdersIndexController
*/
onPressLiveMapDrawerToggle() {
if (this.drawer) {
this.drawer.toggleMinimize({
onToggle: (drawerApi) => {
this.drawerOpen = drawerApi.isMinimized ? 0 : 1;
},
});
}
}
/**
* Handles the resize end event for the `<LiveMapDrawer />` component.
*
* @params {Object} event
* @params {Object.drawerPanelNode|HTMLElement}
* @memberof OperationsOrdersIndexController
*/
onDrawerResizeEnd({ drawerPanelNode }) {
const rect = drawerPanelNode.getBoundingClientRect();
if (rect.height === 0) {
this.drawerOpen = 0;
} else if (rect.height > 1) {
this.drawerOpen = 1;
}
}
onDrawerTabChanged(tabName) {
this.drawerTab = tabName;
}
/**
* Exports all orders.
* @action
* @memberof OperationsOrdersIndexController
*/
exportOrders() {
const selections = this.table.selectedRows.map((_) => _.id);
this.crud.export('order', { params: { selections } });
}
/**
* Redirects to the new order creation page.
* @action
* @memberof OperationsOrdersIndexController
*/
createOrder() {
return this.transitionToRoute('operations.orders.index.new');
}
/**
* Redirects to the view page of a specific order.
* @param {Object} order - The order to view.
* @action
* @memberof OperationsOrdersIndexController
*/
viewOrder(order) {
return this.transitionToRoute('operations.orders.index.view', order);
}
/**
* Cancels a specific order after confirmation.
* @param {Object} order - The order to cancel.
* @param {Object} [options={}] - Additional options for the modal.
* @action
* @memberof OperationsOrdersIndexController
*/
cancelOrder(order, options = {}) {
this.modalsManager.confirm({
title: this.intl.t('fleet-ops.operations.orders.index.cancel-title'),
body: this.intl.t('fleet-ops.operations.orders.index.cancel-body'),
order,
confirm: async (modal) => {
modal.startLoading();
try {
await this.fetch.patch('orders/cancel', { order: order.id });
order.set('status', 'canceled');
this.notifications.success(this.intl.t('fleet-ops.operations.orders.index.cancel-success', { orderId: order.public_id }));
modal.done();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
...options,
});
}
/**
* Dispatches a specific order after confirmation.
* @param {Object} order - The order to dispatch.
* @param {Object} [options={}] - Additional options for the modal.
* @action
* @memberof OperationsOrdersIndexController
*/
dispatchOrder(order, options = {}) {
this.modalsManager.confirm({
title: this.intl.t('fleet-ops.operations.orders.index.dispatch-title'),
body: this.intl.t('fleet-ops.operations.orders.index.dispatch-body'),
acceptButtonScheme: 'primary',
acceptButtonText: 'Dispatch',
acceptButtonIcon: 'paper-plane',
order,
confirm: async (modal) => {
modal.startLoading();
try {
await this.fetch.patch('orders/dispatch', { order: order.id });
order.set('status', 'dispatched');
this.notifications.success(this.intl.t('fleet-ops.operations.orders.index.dispatch-success', { orderId: order.public_id }));
modal.done();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
...options,
});
}
/**
* Deletes a specific order.
* @param {Object} order - The order to delete.
* @param {Object} [options={}] - Additional options for deletion.
* @action
* @memberof OperationsOrdersIndexController
*/
deleteOrder(order, options = {}) {
this.crud.delete(order, {
onSuccess: () => {
return this.hostRouter.refresh();
},
...options,
});
}
/**
* Deletes multiple selected orders.
* @param {Array} [selected=[]] - Orders selected for deletion.
* @action
* @memberof OperationsOrdersIndexController
*/
bulkDeleteOrders(selected = []) {
selected = selected.length > 0 ? selected : this.table.selectedRows;
this.crud.bulkDelete(selected, {
modelNamePath: `public_id`,
acceptButtonText: 'Delete Orders',
onSuccess: async () => {
await this.hostRouter.refresh();
this.table.untoggleSelectAll();
},
});
}
/**
* Cancels multiple selected orders.
*
* @param {Array} [selected=[]] - Orders selected for cancellation.
* @action
* @memberof OperationsOrdersIndexController
*/
bulkCancelOrders(selected = []) {
selected = selected.length > 0 ? selected : this.table.selectedRows;
if (!isArray(selected) || selected.length === 0) {
return;
}
this.crud.bulkAction('cancel', selected, {
acceptButtonText: 'Cancel Orders',
acceptButtonScheme: 'danger',
acceptButtonIcon: 'ban',
modelNamePath: `public_id`,
actionPath: `orders/bulk-cancel`,
actionMethod: `PATCH`,
onConfirm: (canceledOrders) => {
canceledOrders.forEach((order) => {
order.set('status', 'canceled');
});
},
onSuccess: async () => {
await this.hostRouter.refresh();
this.table.untoggleSelectAll();
},
});
}
/**
* Dispatches multiple selected orders.
*
* @param {Array} [selected=[]] - Orders selected for dispatch.
* @action
* @memberof OperationsOrdersIndexController
*/
bulkDispatchOrders(selected = []) {
selected = selected.length > 0 ? selected : this.table.selectedRows;
if (!isArray(selected) || selected.length === 0) {
return;
}
this.crud.bulkAction('dispatch', selected, {
acceptButtonText: 'Dispatch Orders',
acceptButtonScheme: 'magic',
acceptButtonIcon: 'rocket',
modelNamePath: 'public_id',
actionPath: 'orders/bulk-dispatch',
actionMethod: 'POST',
onConfirm: (dispatchedOrders) => {
dispatchedOrders.forEach((order) => {
order.set('status', 'dispatched');
});
},
onSuccess: async () => {
await this.hostRouter.refresh();
this.table.untoggleSelectAll();
},
});
}
/**
* Triggers when the map container is ready.
* @action
* @memberof OperationsOrdersIndexController
*/
onMapContainerReady() {
this.fetchActiveOrdersCount();
}
/**
* Fetches the count of active orders.
* @action
* @memberof OperationsOrdersIndexController
*/
fetchActiveOrdersCount() {
this.fetch.get('fleet-ops/metrics', { discover: ['orders_in_progress'] }).then((response) => {
this.activeOrdersCount = response.orders_in_progress;
});
}
}