@fleetbase/fleetops-engine
Version:
Fleet & Transport Management Extension for Fleetbase
655 lines (517 loc) • 20.4 kB
JavaScript
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { registerDestructor } from '@ember/destroyable';
import { task } from 'ember-concurrency';
export default class LayoutFleetOpsSidebarOperationsMonitorComponent extends Component {
store;
fetch;
universe;
hostRouter;
mapManager;
driverActions;
vehicleActions;
fleetActions;
notifications;
activeTab = 'fleets';
query = '';
fallbackDrivers = [];
fallbackVehicles = [];
fallbackFleets = [];
expandedFleetIds = new Set();
monitorElement;
listElement;
resizeObserver;
resizeFrame;
constructor() {
super(...arguments);
registerDestructor(this, () => this.teardownLayoutObservers());
this.loadFallbackResources.perform();
this.listenForChanges();
}
get driverSource() {
return this.liveDrivers.length ? this.liveDrivers : this.fallbackDrivers;
}
get vehicleSource() {
return this.liveVehicles.length ? this.liveVehicles : this.fallbackVehicles;
}
get drivers() {
return this.sortOnlineFirst(this.filterResources(this.driverSource, ['name', 'public_id', 'status']));
}
get vehicles() {
return this.sortOnlineFirst(this.filterResources(this.vehicleSource, ['display_name', 'name', 'public_id', 'status']));
}
get fleets() {
return this.fallbackFleets;
}
get fleetRows() {
if (this.hasQuery) {
return this.buildFilteredFleetRows(this.fleets);
}
return this.buildFleetRows(this.fleets);
}
get hasQuery() {
return this.query.trim().length > 0;
}
get normalizedQuery() {
return this.query.trim().toLowerCase();
}
get liveDrivers() {
return this.mapManager.livemap?.drivers ?? [];
}
get liveVehicles() {
return this.mapManager.livemap?.vehicles ?? [];
}
get onlineDriverCount() {
return this.driverSource.filter((driver) => driver.online).length;
}
get onlineVehicleCount() {
return this.vehicleSource.filter((vehicle) => vehicle.online).length;
}
get driverOnlineSummary() {
return `${this.onlineDriverCount} drivers online`;
}
get vehicleOnlineSummary() {
return `${this.onlineVehicleCount} vehicles online`;
}
get activeResources() {
return this[this.activeTab] ?? [];
}
get emptyMessage() {
if (this.query) {
return 'No resources match this search.';
}
return `No ${this.activeTab} available.`;
}
get emptyState() {
if (this.hasQuery) {
return {
icon: 'magnifying-glass',
title: 'No resources match this search',
description: 'Try another driver, vehicle, fleet, public ID, status, or assignment.',
action: 'Clear filter',
};
}
return {
drivers: {
icon: 'id-card',
title: 'No drivers yet',
description: 'Create drivers so dispatchers can assign orders and track active work on the live map.',
action: 'Create driver',
},
vehicles: {
icon: 'truck',
title: 'No vehicles yet',
description: 'Add vehicles to power live map visibility, driver assignments, and fleet tracking.',
action: 'Create vehicle',
},
fleets: {
icon: 'user-group',
title: 'No fleets yet',
description: 'Create fleets to organize drivers and vehicles into operational groups.',
action: 'Create fleet',
},
}[this.activeTab];
}
get tabs() {
return [
{ id: 'fleets', label: 'Fleets' },
{ id: 'drivers', label: 'Drivers' },
{ id: 'vehicles', label: 'Vehicles' },
];
}
filterResources(resources = [], fields = []) {
const query = this.normalizedQuery;
if (!query) {
return resources;
}
return resources.filter((resource) => {
return fields.some((field) =>
String(resource[field] ?? '')
.toLowerCase()
.includes(query)
);
});
}
resourceMatches(resource, fields = [], extraValues = []) {
const query = this.normalizedQuery;
if (!query) {
return true;
}
const fieldValues = fields.map((field) => resource?.[field]);
return [...fieldValues, ...extraValues].some((value) =>
String(value ?? '')
.toLowerCase()
.includes(query)
);
}
displayName(resource) {
return resource.displayName ?? resource.display_name ?? resource.name ?? resource.public_id;
}
subtitle(resource) {
return resource.vehicle_name ?? resource.driver_name ?? resource.public_id ?? resource.status;
}
fleetKey(fleet) {
return fleet?.id ?? fleet?.uuid ?? fleet?.public_id ?? fleet?.name;
}
resourceArray(resources) {
if (!resources) {
return [];
}
if (typeof resources.toArray === 'function') {
return resources.toArray();
}
return Array.isArray(resources) ? resources : [];
}
fleetDrivers(fleet) {
return this.resolveFleetResources(fleet, 'driver');
}
fleetVehicles(fleet) {
return this.resolveFleetResources(fleet, 'vehicle');
}
fleetSubfleets(fleet) {
return this.resourceArray(fleet?.subfleets);
}
fleetDriverCount(fleet) {
return Number(fleet?.drivers_count ?? fleet?.driver_count ?? this.fleetDrivers(fleet).length ?? 0);
}
fleetVehicleCount(fleet) {
return Number(fleet?.vehicles_count ?? fleet?.vehicle_count ?? this.fleetVehicles(fleet).length ?? 0);
}
fleetSubtitle(fleet) {
return `${this.fleetDriverCount(fleet)} drivers - ${this.fleetVehicleCount(fleet)} vehicles`;
}
isFleetExpanded(fleet) {
return this.expandedFleetIds.has(this.fleetKey(fleet));
}
fleetHasChildren(fleet) {
return this.fleetSubfleets(fleet).length > 0 || this.fleetDrivers(fleet).length > 0 || this.fleetVehicles(fleet).length > 0;
}
buildFleetRows(fleets = [], depth = 0) {
return fleets.flatMap((fleet) => {
const rows = [{ type: 'fleet', fleet, depth }];
if (!this.isFleetExpanded(fleet)) {
return rows;
}
const childDepth = depth + 1;
this.fleetSubfleets(fleet).forEach((subfleet) => {
rows.push(...this.buildFleetRows([subfleet], childDepth));
});
this.fleetDrivers(fleet).forEach((driver) => {
rows.push({ type: 'driver', driver, depth: childDepth });
});
this.fleetVehicles(fleet).forEach((vehicle) => {
rows.push({ type: 'vehicle', vehicle, depth: childDepth });
});
return rows;
});
}
buildFilteredFleetRows(fleets = [], depth = 0) {
return fleets.flatMap((fleet) => {
const fleetMatches = this.fleetMatches(fleet);
if (fleetMatches) {
return this.buildExpandedFleetRows(fleet, depth);
}
const childDepth = depth + 1;
const matchingSubfleetRows = this.buildFilteredFleetRows(this.fleetSubfleets(fleet), childDepth);
const matchingDriverRows = this.fleetDrivers(fleet)
.filter((driver) => this.driverMatches(driver))
.map((driver) => ({ type: 'driver', driver, depth: childDepth }));
const matchingVehicleRows = this.fleetVehicles(fleet)
.filter((vehicle) => this.vehicleMatches(vehicle))
.map((vehicle) => ({ type: 'vehicle', vehicle, depth: childDepth }));
if (!matchingSubfleetRows.length && !matchingDriverRows.length && !matchingVehicleRows.length) {
return [];
}
return [{ type: 'fleet', fleet, depth }, ...matchingSubfleetRows, ...matchingDriverRows, ...matchingVehicleRows];
});
}
buildExpandedFleetRows(fleet, depth = 0) {
const rows = [{ type: 'fleet', fleet, depth }];
const childDepth = depth + 1;
this.fleetSubfleets(fleet).forEach((subfleet) => {
rows.push(...this.buildExpandedFleetRows(subfleet, childDepth));
});
this.fleetDrivers(fleet).forEach((driver) => {
rows.push({ type: 'driver', driver, depth: childDepth });
});
this.fleetVehicles(fleet).forEach((vehicle) => {
rows.push({ type: 'vehicle', vehicle, depth: childDepth });
});
return rows;
}
fleetMatches(fleet) {
return this.resourceMatches(
fleet,
['name', 'public_id', 'status', 'slug'],
[this.fleetSubtitle(fleet), this.fleetDriverCount(fleet), this.fleetVehicleCount(fleet), fleet?.drivers_online_count, fleet?.vehicles_online_count]
);
}
driverMatches(driver) {
return this.resourceMatches(driver, ['displayName', 'display_name', 'name', 'public_id', 'status', 'vehicle_name']);
}
vehicleMatches(vehicle) {
return this.resourceMatches(vehicle, ['displayName', 'display_name', 'name', 'public_id', 'status', 'driver_name', 'plate_number', 'vin']);
}
collectFleetKeys(fleets = []) {
return fleets.flatMap((fleet) => {
const key = this.fleetKey(fleet);
return [key, ...this.collectFleetKeys(this.fleetSubfleets(fleet))].filter(Boolean);
});
}
sortOnlineFirst(resources = []) {
return [...resources].sort((a, b) => {
const onlineSort = Number(Boolean(b.online)) - Number(Boolean(a.online));
if (onlineSort !== 0) {
return onlineSort;
}
return String(this.displayName(a) ?? '').localeCompare(String(this.displayName(b) ?? ''));
});
}
get driverById() {
return this.resourcesById(this.fallbackDrivers);
}
get vehicleById() {
return this.resourcesById(this.fallbackVehicles);
}
resourcesById(resources = []) {
return this.resourceArray(resources).reduce((map, resource) => {
this.resourceIdentifiers(resource).forEach((id) => map.set(id, resource));
return map;
}, new Map());
}
resourceIdentifiers(resource) {
return [resource?.id, resource?.uuid, resource?.public_id].filter(Boolean).map((id) => String(id));
}
resolveFleetResources(fleet, type) {
const embeddedResources = this.resourceArray(fleet?.[`${type}s`]);
if (embeddedResources.length) {
return embeddedResources;
}
const ids = this.resourceArray(fleet?.[`${type}_ids`]);
const resources = type === 'driver' ? this.driverById : this.vehicleById;
return ids.map((id) => resources.get(String(id))).filter(Boolean);
}
normalizeMonitorResource(type, resource) {
if (!resource) {
return resource;
}
try {
this.store.pushPayload?.(type, { [type]: resource });
} catch (_) {
// Keep the monitor usable in tests and older hosts without pushPayload support.
}
return this.store.peekRecord?.(type, resource.id) ?? this.store.peekRecord?.(type, resource.public_id) ?? resource;
}
listenForChanges() {
this.universe.on('fleet-ops.driver.saved', () => this.loadFallbackResources.perform());
this.universe.on('fleet-ops.vehicle.saved', () => this.loadFallbackResources.perform());
this.universe.on('fleet-ops.fleet.vehicle_assigned', () => this.loadFallbackResources.perform());
this.universe.on('fleet-ops.fleet.vehicle_unassigned', () => this.loadFallbackResources.perform());
this.universe.on('fleet-ops.fleet.driver_assigned', () => this.loadFallbackResources.perform());
this.universe.on('fleet-ops.fleet.driver_unassigned', () => this.loadFallbackResources.perform());
}
setActiveTab(tab) {
this.activeTab = tab;
this.scheduleListHeightUpdate();
}
updateQuery(event) {
this.query = event.target.value;
this.scheduleListHeightUpdate();
}
clearFilter() {
this.query = '';
this.scheduleListHeightUpdate();
}
performEmptyStateAction() {
if (this.hasQuery) {
this.clearFilter();
return;
}
if (this.activeTab === 'drivers') {
this.driverActions.panel.create();
return;
}
if (this.activeTab === 'vehicles') {
this.vehicleActions.panel.create();
return;
}
if (this.activeTab === 'fleets') {
this.fleetActions.panel.create();
}
}
registerMonitor(element) {
this.monitorElement = element;
this.setupLayoutObservers();
this.scheduleListHeightUpdate();
}
registerList(element) {
this.listElement = element;
this.scheduleListHeightUpdate();
}
setupLayoutObservers() {
this.teardownLayoutObservers();
if (typeof window !== 'undefined') {
window.addEventListener('resize', this.scheduleListHeightUpdate);
}
if (typeof ResizeObserver === 'undefined' || !this.monitorElement) {
return;
}
this.resizeObserver = new ResizeObserver(() => this.scheduleListHeightUpdate());
this.resizeObserver.observe(this.monitorElement);
const sidebarContentInner = this.monitorElement.closest('.next-sidebar-content-inner');
const navigator = this.monitorElement.closest('.next-sidebar-navigator');
if (sidebarContentInner) {
this.resizeObserver.observe(sidebarContentInner);
}
if (navigator) {
this.resizeObserver.observe(navigator);
}
}
teardownLayoutObservers() {
if (typeof window !== 'undefined') {
window.removeEventListener('resize', this.scheduleListHeightUpdate);
}
if (this.resizeFrame) {
cancelAnimationFrame(this.resizeFrame);
this.resizeFrame = undefined;
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = undefined;
}
}
scheduleListHeightUpdate() {
if (typeof requestAnimationFrame === 'undefined') {
this.updateListHeight();
return;
}
if (this.resizeFrame) {
cancelAnimationFrame(this.resizeFrame);
}
this.resizeFrame = requestAnimationFrame(() => {
this.resizeFrame = undefined;
this.updateListHeight();
});
}
updateListHeight() {
if (!this.listElement || !this.monitorElement) {
return;
}
const boundary = this.listElement.closest('.next-sidebar-content-inner') ?? this.listElement.closest('.next-sidebar-content') ?? this.monitorElement.parentElement;
if (!boundary) {
return;
}
const listRect = this.listElement.getBoundingClientRect();
const boundaryRect = boundary.getBoundingClientRect();
const availableHeight = Math.floor(boundaryRect.bottom - listRect.top - 10);
const height = Math.max(128, availableHeight);
this.listElement.style.setProperty('--fleet-ops-operations-monitor-list-height', `${height}px`);
}
toggleFleet(fleet) {
const key = this.fleetKey(fleet);
const expandedFleetIds = new Set(this.expandedFleetIds);
if (expandedFleetIds.has(key)) {
expandedFleetIds.delete(key);
} else {
expandedFleetIds.add(key);
}
this.expandedFleetIds = expandedFleetIds;
}
calculateDropdownPosition(trigger) {
const { top, right } = trigger.getBoundingClientRect();
return {
style: {
left: right + window.scrollX,
top: top + window.scrollY - 4,
},
};
}
viewDriver(driver) {
this.driverActions.panel.view(driver);
}
editDriver(driver) {
this.driverActions.panel.edit(driver, { useDefaultSaveTask: true });
}
assignOrderToDriver(driver) {
this.driverActions.assignOrder(driver);
}
assignVehicleToDriver(driver) {
this.driverActions.assignVehicle(driver);
}
deleteDriver(driver) {
this.driverActions.delete(driver);
}
viewVehicle(vehicle) {
this.vehicleActions.panel.view(vehicle);
}
editVehicle(vehicle) {
this.vehicleActions.panel.edit(vehicle, { useDefaultSaveTask: true });
}
deleteVehicle(vehicle) {
this.vehicleActions.delete(vehicle);
}
viewFleet(fleet) {
this.fleetActions.panel.view(fleet);
}
assignDriverToFleet(fleet) {
this.fleetActions.assignDriver(fleet);
}
assignVehicleToFleet(fleet) {
this.fleetActions.assignVehicle(fleet);
}
async locateDriver(driver) {
await this.transitionToLiveMap();
await this.focusResource(driver, () => this.driverActions.panel.view(driver, { closeOnTransition: true }));
}
async locateVehicle(vehicle) {
await this.transitionToLiveMap();
await this.focusResource(vehicle, () => this.vehicleActions.panel.view(vehicle, { closeOnTransition: true }));
}
async transitionToLiveMap() {
try {
await this.hostRouter.transitionTo('console.fleet-ops.operations.orders.index', { queryParams: { layout: 'map' } });
} catch (_) {
// Keep locate actions usable even if the current transition is already in-flight.
}
}
async focusResource(resource, moveend) {
if (!resource) {
return;
}
await this.mapManager.waitForMap({ timeoutMs: 8000 });
this.mapManager.focusResource(resource, 16, {
paddingBottomRight: [300, 200],
moveend,
});
}
*loadFallbackResources() {
try {
const response = yield this.fetch.get('fleet-ops/live/operations-monitor', {}, { namespace: 'int/v1' });
const drivers = this.resourceArray(response?.drivers).map((driver) => this.normalizeMonitorResource('driver', driver));
const vehicles = this.resourceArray(response?.vehicles).map((vehicle) => this.normalizeMonitorResource('vehicle', vehicle));
const fleets = this.resourceArray(response?.fleets).map((fleet) => this.normalizeMonitorFleet(fleet));
this.fallbackDrivers = drivers;
this.fallbackVehicles = vehicles;
this.fallbackFleets = fleets;
this.expandedFleetIds = new Set(this.collectFleetKeys(this.fallbackFleets));
} catch (error) {
this.notifications.serverError(error);
}
}
normalizeMonitorFleet(fleet) {
const normalizedFleet = this.normalizeMonitorResource('fleet', fleet);
const subfleets = this.resourceArray(fleet?.subfleets).map((subfleet) => this.normalizeMonitorFleet(subfleet));
if (normalizedFleet?.set) {
normalizedFleet.set('driver_ids', this.resourceArray(fleet?.driver_ids));
normalizedFleet.set('vehicle_ids', this.resourceArray(fleet?.vehicle_ids));
normalizedFleet.set('subfleets', subfleets);
return normalizedFleet;
}
return {
...fleet,
subfleets,
};
}
}