@fleetbase/fleetops-engine 0.6.52 → 0.6.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/components/layout/fleet-ops-sidebar/operations-monitor.hbs +201 -0
- package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +589 -0
- package/addon/components/layout/fleet-ops-sidebar.hbs +19 -233
- package/addon/components/layout/fleet-ops-sidebar.js +287 -394
- package/addon/components/maintenance-schedule/form.js +1 -0
- package/addon/components/order/form/payload.js +3 -1
- package/addon/controllers/connectivity/devices/index.js +4 -1
- package/addon/controllers/maintenance/index.js +100 -0
- package/addon/controllers/management/fuel-transactions/index.js +1 -1
- package/addon/controllers/management/index.js +88 -0
- package/addon/controllers/settings/index.js +142 -0
- package/addon/extension.js +21 -0
- package/addon/routes/analytics/index.js +3 -0
- package/addon/routes/connectivity/devices/index.js +3 -0
- package/addon/routes/maintenance/index.js +3 -0
- package/addon/routes/maintenance/work-orders/index.js +2 -0
- package/addon/routes/settings/index.js +3 -0
- package/addon/styles/fleetops-engine.css +1801 -2
- package/addon/templates/analytics/index.hbs +14 -0
- package/addon/templates/analytics/reports/index.hbs +4 -4
- package/addon/templates/maintenance/index.hbs +113 -0
- package/addon/templates/management/fuel-transactions/index/details.hbs +1 -1
- package/addon/templates/management/index.hbs +109 -73
- package/addon/templates/operations/orders/index.hbs +1 -1
- package/addon/templates/operations/service-rates/index.hbs +1 -1
- package/addon/templates/settings/index.hbs +83 -0
- package/addon/templates/settings/routing.hbs +2 -2
- package/app/controllers/maintenance/index.js +1 -0
- package/app/controllers/management/index.js +1 -0
- package/app/controllers/settings/index.js +1 -0
- package/app/routes/analytics/index.js +1 -0
- package/app/routes/maintenance/index.js +1 -0
- package/app/routes/settings/index.js +1 -0
- package/app/templates/analytics/index.js +1 -0
- package/app/templates/maintenance/index.js +1 -0
- package/app/templates/management/index.js +1 -0
- package/app/templates/settings/index.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php +30 -0
- package/server/src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php +1 -0
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +75 -11
- package/server/src/Http/Controllers/Internal/v1/HubController.php +300 -0
- package/server/src/Http/Controllers/Internal/v1/SearchController.php +506 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +94 -10
- package/server/src/Http/Filter/DeviceFilter.php +74 -0
- package/server/src/Http/Filter/DriverFilter.php +6 -0
- package/server/src/Http/Filter/VehicleFilter.php +6 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +82 -24
- package/server/src/Models/Device.php +1 -1
- package/server/src/Models/Driver.php +1 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +51 -13
- package/server/src/routes.php +9 -0
- package/server/tests/HubRoutesTest.php +62 -0
- package/server/tests/TelematicsHardeningTest.php +94 -3
- package/translations/ar-ae.yml +1 -1
- package/translations/vi-vn.yaml +1 -1
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
import { action } from '@ember/object';
|
|
5
|
+
import { registerDestructor } from '@ember/destroyable';
|
|
6
|
+
import { task } from 'ember-concurrency';
|
|
7
|
+
|
|
8
|
+
export default class LayoutFleetOpsSidebarOperationsMonitorComponent extends Component {
|
|
9
|
+
@service store;
|
|
10
|
+
@service universe;
|
|
11
|
+
@service hostRouter;
|
|
12
|
+
@service mapManager;
|
|
13
|
+
@service driverActions;
|
|
14
|
+
@service vehicleActions;
|
|
15
|
+
@service fleetActions;
|
|
16
|
+
@service notifications;
|
|
17
|
+
|
|
18
|
+
@tracked activeTab = 'fleets';
|
|
19
|
+
@tracked query = '';
|
|
20
|
+
@tracked fallbackDrivers = [];
|
|
21
|
+
@tracked fallbackVehicles = [];
|
|
22
|
+
@tracked fallbackFleets = [];
|
|
23
|
+
@tracked expandedFleetIds = new Set();
|
|
24
|
+
|
|
25
|
+
monitorElement;
|
|
26
|
+
listElement;
|
|
27
|
+
resizeObserver;
|
|
28
|
+
resizeFrame;
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
super(...arguments);
|
|
32
|
+
registerDestructor(this, () => this.teardownLayoutObservers());
|
|
33
|
+
this.loadFallbackResources.perform();
|
|
34
|
+
this.listenForChanges();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get driverSource() {
|
|
38
|
+
return this.liveDrivers.length ? this.liveDrivers : this.fallbackDrivers;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get vehicleSource() {
|
|
42
|
+
return this.liveVehicles.length ? this.liveVehicles : this.fallbackVehicles;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get drivers() {
|
|
46
|
+
return this.sortOnlineFirst(this.filterResources(this.driverSource, ['name', 'public_id', 'status']));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get vehicles() {
|
|
50
|
+
return this.sortOnlineFirst(this.filterResources(this.vehicleSource, ['display_name', 'name', 'public_id', 'status']));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get fleets() {
|
|
54
|
+
return this.fallbackFleets;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get fleetRows() {
|
|
58
|
+
if (this.hasQuery) {
|
|
59
|
+
return this.buildFilteredFleetRows(this.fleets);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return this.buildFleetRows(this.fleets);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get hasQuery() {
|
|
66
|
+
return this.query.trim().length > 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get normalizedQuery() {
|
|
70
|
+
return this.query.trim().toLowerCase();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get liveDrivers() {
|
|
74
|
+
return this.mapManager.livemap?.drivers ?? [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get liveVehicles() {
|
|
78
|
+
return this.mapManager.livemap?.vehicles ?? [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get onlineDriverCount() {
|
|
82
|
+
return this.driverSource.filter((driver) => driver.online).length;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get onlineVehicleCount() {
|
|
86
|
+
return this.vehicleSource.filter((vehicle) => vehicle.online).length;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get driverOnlineSummary() {
|
|
90
|
+
return `${this.onlineDriverCount} drivers online`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get vehicleOnlineSummary() {
|
|
94
|
+
return `${this.onlineVehicleCount} vehicles online`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get activeResources() {
|
|
98
|
+
return this[this.activeTab] ?? [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get emptyMessage() {
|
|
102
|
+
if (this.query) {
|
|
103
|
+
return 'No resources match this search.';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return `No ${this.activeTab} available.`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get emptyState() {
|
|
110
|
+
if (this.hasQuery) {
|
|
111
|
+
return {
|
|
112
|
+
icon: 'magnifying-glass',
|
|
113
|
+
title: 'No resources match this search',
|
|
114
|
+
description: 'Try another driver, vehicle, fleet, public ID, status, or assignment.',
|
|
115
|
+
action: 'Clear filter',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
drivers: {
|
|
121
|
+
icon: 'id-card',
|
|
122
|
+
title: 'No drivers yet',
|
|
123
|
+
description: 'Create drivers so dispatchers can assign orders and track active work on the live map.',
|
|
124
|
+
action: 'Create driver',
|
|
125
|
+
},
|
|
126
|
+
vehicles: {
|
|
127
|
+
icon: 'truck',
|
|
128
|
+
title: 'No vehicles yet',
|
|
129
|
+
description: 'Add vehicles to power live map visibility, driver assignments, and fleet tracking.',
|
|
130
|
+
action: 'Create vehicle',
|
|
131
|
+
},
|
|
132
|
+
fleets: {
|
|
133
|
+
icon: 'user-group',
|
|
134
|
+
title: 'No fleets yet',
|
|
135
|
+
description: 'Create fleets to organize drivers and vehicles into operational groups.',
|
|
136
|
+
action: 'Create fleet',
|
|
137
|
+
},
|
|
138
|
+
}[this.activeTab];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get tabs() {
|
|
142
|
+
return [
|
|
143
|
+
{ id: 'fleets', label: 'Fleets' },
|
|
144
|
+
{ id: 'drivers', label: 'Drivers' },
|
|
145
|
+
{ id: 'vehicles', label: 'Vehicles' },
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
filterResources(resources = [], fields = []) {
|
|
150
|
+
const query = this.normalizedQuery;
|
|
151
|
+
|
|
152
|
+
if (!query) {
|
|
153
|
+
return resources;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return resources.filter((resource) => {
|
|
157
|
+
return fields.some((field) =>
|
|
158
|
+
String(resource[field] ?? '')
|
|
159
|
+
.toLowerCase()
|
|
160
|
+
.includes(query)
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
resourceMatches(resource, fields = [], extraValues = []) {
|
|
166
|
+
const query = this.normalizedQuery;
|
|
167
|
+
|
|
168
|
+
if (!query) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const fieldValues = fields.map((field) => resource?.[field]);
|
|
173
|
+
|
|
174
|
+
return [...fieldValues, ...extraValues].some((value) =>
|
|
175
|
+
String(value ?? '')
|
|
176
|
+
.toLowerCase()
|
|
177
|
+
.includes(query)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
displayName(resource) {
|
|
182
|
+
return resource.displayName ?? resource.display_name ?? resource.name ?? resource.public_id;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
subtitle(resource) {
|
|
186
|
+
return resource.vehicle_name ?? resource.driver_name ?? resource.public_id ?? resource.status;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fleetKey(fleet) {
|
|
190
|
+
return fleet?.id ?? fleet?.uuid ?? fleet?.public_id ?? fleet?.name;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
resourceArray(resources) {
|
|
194
|
+
if (!resources) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (typeof resources.toArray === 'function') {
|
|
199
|
+
return resources.toArray();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return Array.isArray(resources) ? resources : [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fleetDrivers(fleet) {
|
|
206
|
+
return this.resourceArray(fleet?.drivers);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fleetVehicles(fleet) {
|
|
210
|
+
return this.resourceArray(fleet?.vehicles);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fleetSubfleets(fleet) {
|
|
214
|
+
return this.resourceArray(fleet?.subfleets);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fleetDriverCount(fleet) {
|
|
218
|
+
return Number(fleet?.drivers_count ?? fleet?.driver_count ?? this.fleetDrivers(fleet).length ?? 0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fleetVehicleCount(fleet) {
|
|
222
|
+
return Number(fleet?.vehicles_count ?? fleet?.vehicle_count ?? this.fleetVehicles(fleet).length ?? 0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@action fleetSubtitle(fleet) {
|
|
226
|
+
return `${this.fleetDriverCount(fleet)} drivers - ${this.fleetVehicleCount(fleet)} vehicles`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@action isFleetExpanded(fleet) {
|
|
230
|
+
return this.expandedFleetIds.has(this.fleetKey(fleet));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@action fleetHasChildren(fleet) {
|
|
234
|
+
return this.fleetSubfleets(fleet).length > 0 || this.fleetDrivers(fleet).length > 0 || this.fleetVehicles(fleet).length > 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
buildFleetRows(fleets = [], depth = 0) {
|
|
238
|
+
return fleets.flatMap((fleet) => {
|
|
239
|
+
const rows = [{ type: 'fleet', fleet, depth }];
|
|
240
|
+
|
|
241
|
+
if (!this.isFleetExpanded(fleet)) {
|
|
242
|
+
return rows;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const childDepth = depth + 1;
|
|
246
|
+
|
|
247
|
+
this.fleetSubfleets(fleet).forEach((subfleet) => {
|
|
248
|
+
rows.push(...this.buildFleetRows([subfleet], childDepth));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
this.fleetDrivers(fleet).forEach((driver) => {
|
|
252
|
+
rows.push({ type: 'driver', driver, depth: childDepth });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
this.fleetVehicles(fleet).forEach((vehicle) => {
|
|
256
|
+
rows.push({ type: 'vehicle', vehicle, depth: childDepth });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return rows;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
buildFilteredFleetRows(fleets = [], depth = 0) {
|
|
264
|
+
return fleets.flatMap((fleet) => {
|
|
265
|
+
const fleetMatches = this.fleetMatches(fleet);
|
|
266
|
+
|
|
267
|
+
if (fleetMatches) {
|
|
268
|
+
return this.buildExpandedFleetRows(fleet, depth);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const childDepth = depth + 1;
|
|
272
|
+
const matchingSubfleetRows = this.buildFilteredFleetRows(this.fleetSubfleets(fleet), childDepth);
|
|
273
|
+
const matchingDriverRows = this.fleetDrivers(fleet)
|
|
274
|
+
.filter((driver) => this.driverMatches(driver))
|
|
275
|
+
.map((driver) => ({ type: 'driver', driver, depth: childDepth }));
|
|
276
|
+
const matchingVehicleRows = this.fleetVehicles(fleet)
|
|
277
|
+
.filter((vehicle) => this.vehicleMatches(vehicle))
|
|
278
|
+
.map((vehicle) => ({ type: 'vehicle', vehicle, depth: childDepth }));
|
|
279
|
+
|
|
280
|
+
if (!matchingSubfleetRows.length && !matchingDriverRows.length && !matchingVehicleRows.length) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return [{ type: 'fleet', fleet, depth }, ...matchingSubfleetRows, ...matchingDriverRows, ...matchingVehicleRows];
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
buildExpandedFleetRows(fleet, depth = 0) {
|
|
289
|
+
const rows = [{ type: 'fleet', fleet, depth }];
|
|
290
|
+
const childDepth = depth + 1;
|
|
291
|
+
|
|
292
|
+
this.fleetSubfleets(fleet).forEach((subfleet) => {
|
|
293
|
+
rows.push(...this.buildExpandedFleetRows(subfleet, childDepth));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
this.fleetDrivers(fleet).forEach((driver) => {
|
|
297
|
+
rows.push({ type: 'driver', driver, depth: childDepth });
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
this.fleetVehicles(fleet).forEach((vehicle) => {
|
|
301
|
+
rows.push({ type: 'vehicle', vehicle, depth: childDepth });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return rows;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
fleetMatches(fleet) {
|
|
308
|
+
return this.resourceMatches(
|
|
309
|
+
fleet,
|
|
310
|
+
['name', 'public_id', 'status', 'slug'],
|
|
311
|
+
[this.fleetSubtitle(fleet), this.fleetDriverCount(fleet), this.fleetVehicleCount(fleet), fleet?.drivers_online_count, fleet?.vehicles_online_count]
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
driverMatches(driver) {
|
|
316
|
+
return this.resourceMatches(driver, ['displayName', 'display_name', 'name', 'public_id', 'status', 'vehicle_name']);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
vehicleMatches(vehicle) {
|
|
320
|
+
return this.resourceMatches(vehicle, ['displayName', 'display_name', 'name', 'public_id', 'status', 'driver_name', 'plate_number', 'vin']);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
collectFleetKeys(fleets = []) {
|
|
324
|
+
return fleets.flatMap((fleet) => {
|
|
325
|
+
const key = this.fleetKey(fleet);
|
|
326
|
+
return [key, ...this.collectFleetKeys(this.fleetSubfleets(fleet))].filter(Boolean);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
sortOnlineFirst(resources = []) {
|
|
331
|
+
return [...resources].sort((a, b) => {
|
|
332
|
+
const onlineSort = Number(Boolean(b.online)) - Number(Boolean(a.online));
|
|
333
|
+
|
|
334
|
+
if (onlineSort !== 0) {
|
|
335
|
+
return onlineSort;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return String(this.displayName(a) ?? '').localeCompare(String(this.displayName(b) ?? ''));
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
listenForChanges() {
|
|
343
|
+
this.universe.on('fleet-ops.driver.saved', () => this.loadFallbackResources.perform());
|
|
344
|
+
this.universe.on('fleet-ops.vehicle.saved', () => this.loadFallbackResources.perform());
|
|
345
|
+
this.universe.on('fleet-ops.fleet.vehicle_assigned', () => this.loadFallbackResources.perform());
|
|
346
|
+
this.universe.on('fleet-ops.fleet.vehicle_unassigned', () => this.loadFallbackResources.perform());
|
|
347
|
+
this.universe.on('fleet-ops.fleet.driver_assigned', () => this.loadFallbackResources.perform());
|
|
348
|
+
this.universe.on('fleet-ops.fleet.driver_unassigned', () => this.loadFallbackResources.perform());
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@action setActiveTab(tab) {
|
|
352
|
+
this.activeTab = tab;
|
|
353
|
+
this.scheduleListHeightUpdate();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@action updateQuery(event) {
|
|
357
|
+
this.query = event.target.value;
|
|
358
|
+
this.scheduleListHeightUpdate();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
@action clearFilter() {
|
|
362
|
+
this.query = '';
|
|
363
|
+
this.scheduleListHeightUpdate();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
@action performEmptyStateAction() {
|
|
367
|
+
if (this.hasQuery) {
|
|
368
|
+
this.clearFilter();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (this.activeTab === 'drivers') {
|
|
373
|
+
this.driverActions.panel.create();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (this.activeTab === 'vehicles') {
|
|
378
|
+
this.vehicleActions.panel.create();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (this.activeTab === 'fleets') {
|
|
383
|
+
this.fleetActions.panel.create();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@action registerMonitor(element) {
|
|
388
|
+
this.monitorElement = element;
|
|
389
|
+
this.setupLayoutObservers();
|
|
390
|
+
this.scheduleListHeightUpdate();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
@action registerList(element) {
|
|
394
|
+
this.listElement = element;
|
|
395
|
+
this.scheduleListHeightUpdate();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
setupLayoutObservers() {
|
|
399
|
+
this.teardownLayoutObservers();
|
|
400
|
+
|
|
401
|
+
if (typeof window !== 'undefined') {
|
|
402
|
+
window.addEventListener('resize', this.scheduleListHeightUpdate);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (typeof ResizeObserver === 'undefined' || !this.monitorElement) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
this.resizeObserver = new ResizeObserver(() => this.scheduleListHeightUpdate());
|
|
410
|
+
this.resizeObserver.observe(this.monitorElement);
|
|
411
|
+
|
|
412
|
+
const sidebarContentInner = this.monitorElement.closest('.next-sidebar-content-inner');
|
|
413
|
+
const navigator = this.monitorElement.closest('.next-sidebar-navigator');
|
|
414
|
+
|
|
415
|
+
if (sidebarContentInner) {
|
|
416
|
+
this.resizeObserver.observe(sidebarContentInner);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (navigator) {
|
|
420
|
+
this.resizeObserver.observe(navigator);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
teardownLayoutObservers() {
|
|
425
|
+
if (typeof window !== 'undefined') {
|
|
426
|
+
window.removeEventListener('resize', this.scheduleListHeightUpdate);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (this.resizeFrame) {
|
|
430
|
+
cancelAnimationFrame(this.resizeFrame);
|
|
431
|
+
this.resizeFrame = undefined;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (this.resizeObserver) {
|
|
435
|
+
this.resizeObserver.disconnect();
|
|
436
|
+
this.resizeObserver = undefined;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
@action scheduleListHeightUpdate() {
|
|
441
|
+
if (typeof requestAnimationFrame === 'undefined') {
|
|
442
|
+
this.updateListHeight();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (this.resizeFrame) {
|
|
447
|
+
cancelAnimationFrame(this.resizeFrame);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
this.resizeFrame = requestAnimationFrame(() => {
|
|
451
|
+
this.resizeFrame = undefined;
|
|
452
|
+
this.updateListHeight();
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
updateListHeight() {
|
|
457
|
+
if (!this.listElement || !this.monitorElement) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const boundary = this.listElement.closest('.next-sidebar-content-inner') ?? this.listElement.closest('.next-sidebar-content') ?? this.monitorElement.parentElement;
|
|
462
|
+
|
|
463
|
+
if (!boundary) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const listRect = this.listElement.getBoundingClientRect();
|
|
468
|
+
const boundaryRect = boundary.getBoundingClientRect();
|
|
469
|
+
const availableHeight = Math.floor(boundaryRect.bottom - listRect.top - 10);
|
|
470
|
+
const height = Math.max(128, availableHeight);
|
|
471
|
+
|
|
472
|
+
this.listElement.style.setProperty('--fleet-ops-operations-monitor-list-height', `${height}px`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@action toggleFleet(fleet) {
|
|
476
|
+
const key = this.fleetKey(fleet);
|
|
477
|
+
const expandedFleetIds = new Set(this.expandedFleetIds);
|
|
478
|
+
|
|
479
|
+
if (expandedFleetIds.has(key)) {
|
|
480
|
+
expandedFleetIds.delete(key);
|
|
481
|
+
} else {
|
|
482
|
+
expandedFleetIds.add(key);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this.expandedFleetIds = expandedFleetIds;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
@action calculateDropdownPosition(trigger) {
|
|
489
|
+
const { top, right } = trigger.getBoundingClientRect();
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
style: {
|
|
493
|
+
left: right + window.scrollX,
|
|
494
|
+
top: top + window.scrollY - 4,
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
@action viewDriver(driver) {
|
|
500
|
+
this.driverActions.panel.view(driver);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
@action editDriver(driver) {
|
|
504
|
+
this.driverActions.panel.edit(driver, { useDefaultSaveTask: true });
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
@action assignOrderToDriver(driver) {
|
|
508
|
+
this.driverActions.assignOrder(driver);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
@action assignVehicleToDriver(driver) {
|
|
512
|
+
this.driverActions.assignVehicle(driver);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
@action deleteDriver(driver) {
|
|
516
|
+
this.driverActions.delete(driver);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
@action viewVehicle(vehicle) {
|
|
520
|
+
this.vehicleActions.panel.view(vehicle);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
@action editVehicle(vehicle) {
|
|
524
|
+
this.vehicleActions.panel.edit(vehicle, { useDefaultSaveTask: true });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
@action deleteVehicle(vehicle) {
|
|
528
|
+
this.vehicleActions.delete(vehicle);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
@action viewFleet(fleet) {
|
|
532
|
+
this.fleetActions.panel.view(fleet);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
@action assignDriverToFleet(fleet) {
|
|
536
|
+
this.fleetActions.assignDriver(fleet);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
@action assignVehicleToFleet(fleet) {
|
|
540
|
+
this.fleetActions.assignVehicle(fleet);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
@action async locateDriver(driver) {
|
|
544
|
+
await this.transitionToLiveMap();
|
|
545
|
+
await this.focusResource(driver, () => this.driverActions.panel.view(driver, { closeOnTransition: true }));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
@action async locateVehicle(vehicle) {
|
|
549
|
+
await this.transitionToLiveMap();
|
|
550
|
+
await this.focusResource(vehicle, () => this.vehicleActions.panel.view(vehicle, { closeOnTransition: true }));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async transitionToLiveMap() {
|
|
554
|
+
try {
|
|
555
|
+
await this.hostRouter.transitionTo('console.fleet-ops.operations.orders.index', { queryParams: { layout: 'map' } });
|
|
556
|
+
} catch (_) {
|
|
557
|
+
// Keep locate actions usable even if the current transition is already in-flight.
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async focusResource(resource, moveend) {
|
|
562
|
+
if (!resource) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
await this.mapManager.waitForMap({ timeoutMs: 8000 });
|
|
567
|
+
this.mapManager.focusResource(resource, 16, {
|
|
568
|
+
paddingBottomRight: [300, 200],
|
|
569
|
+
moveend,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
@task *loadFallbackResources() {
|
|
574
|
+
try {
|
|
575
|
+
const [drivers, vehicles, fleets] = yield Promise.all([
|
|
576
|
+
this.store.query('driver', { limit: 20, without: ['vendor'] }),
|
|
577
|
+
this.store.query('vehicle', { limit: 20 }),
|
|
578
|
+
this.store.query('fleet', { limit: 20, with: ['vehicles', 'drivers', 'subfleets'], parents_only: true }),
|
|
579
|
+
]);
|
|
580
|
+
|
|
581
|
+
this.fallbackDrivers = drivers.toArray?.() ?? drivers;
|
|
582
|
+
this.fallbackVehicles = vehicles.toArray?.() ?? vehicles;
|
|
583
|
+
this.fallbackFleets = fleets.toArray?.() ?? fleets;
|
|
584
|
+
this.expandedFleetIds = new Set(this.collectFleetKeys(this.fallbackFleets));
|
|
585
|
+
} catch (error) {
|
|
586
|
+
this.notifications.serverError(error);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|