@fleetbase/fleetops-engine 0.6.55 → 0.6.57

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.
Files changed (161) hide show
  1. package/addon/components/admin/map-settings.hbs +1 -1
  2. package/addon/components/ai/create-order-preview.hbs +297 -0
  3. package/addon/components/ai/create-order-preview.js +362 -0
  4. package/addon/components/ai/route-preview-map.hbs +79 -0
  5. package/addon/components/ai/route-preview-map.js +527 -0
  6. package/addon/components/device/details.js +1 -1
  7. package/addon/components/device-event/details.hbs +192 -0
  8. package/addon/components/device-event/details.js +151 -0
  9. package/addon/components/driver/schedule.js +1 -1
  10. package/addon/components/fleet/details.hbs +21 -8
  11. package/addon/components/fleet/details.js +55 -1
  12. package/addon/components/fleet/hierarchy-tree.hbs +137 -0
  13. package/addon/components/fleet/hierarchy-tree.js +528 -0
  14. package/addon/components/layout/fleet-ops-sidebar.js +15 -9
  15. package/addon/components/map/drawer/device-event-listing.js +1 -0
  16. package/addon/components/map/drawer/driver-listing.js +1 -0
  17. package/addon/components/map/drawer/place-listing.js +1 -0
  18. package/addon/components/map/drawer/vehicle-listing.js +1 -0
  19. package/addon/components/order/details.hbs +34 -1
  20. package/addon/components/order/form/details.hbs +4 -0
  21. package/addon/components/order/form.hbs +9 -1
  22. package/addon/components/telematic/details.hbs +1 -1
  23. package/addon/components/telematic/form.hbs +2 -2
  24. package/addon/components/telematic/settings.hbs +1 -1
  25. package/addon/components/vehicle/card.hbs +14 -2
  26. package/addon/components/vehicle/form.hbs +13 -1
  27. package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
  28. package/addon/controllers/connectivity/devices/index/details/vehicle.js +2 -2
  29. package/addon/controllers/connectivity/events/details.js +68 -1
  30. package/addon/controllers/connectivity/sensors/index.js +4 -0
  31. package/addon/controllers/connectivity/telematics/details/events.js +1 -1
  32. package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
  33. package/addon/controllers/settings/map.js +19 -0
  34. package/addon/extension.js +14 -0
  35. package/addon/instance-initializers/register-vroom-allocation.js +1 -1
  36. package/addon/routes/connectivity/events/details.js +26 -1
  37. package/addon/routes/management/fleets/index/details.js +1 -1
  38. package/addon/routes/management/vehicles/index/details.js +3 -1
  39. package/addon/services/map-adapter/google.js +66 -1
  40. package/addon/services/map-adapter/leaflet.js +1 -1
  41. package/addon/services/map-adapter-interface.js +1 -1
  42. package/addon/services/map-manager.js +16 -1
  43. package/addon/services/map-settings.js +15 -0
  44. package/addon/styles/fleetops-engine.css +487 -1
  45. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -1
  46. package/addon/templates/connectivity/events/details.hbs +40 -1
  47. package/addon/templates/settings/map.hbs +38 -9
  48. package/addon/utils/leaflet-plugin-loader.js +5 -5
  49. package/addon/utils/map-drawer-dropdown-position.js +11 -13
  50. package/addon/utils/route-colors.js +1 -1
  51. package/app/components/ai/create-order-preview.js +1 -0
  52. package/app/components/ai/route-preview-map.js +1 -0
  53. package/app/components/device-event/details.hbs +192 -0
  54. package/app/components/device-event/details.js +1 -0
  55. package/app/components/fleet/hierarchy-tree.js +1 -0
  56. package/app/services/route-engine.js +1 -0
  57. package/composer.json +1 -1
  58. package/extension.json +1 -1
  59. package/package.json +6 -4
  60. package/scripts/normalize-rgba-alpha.js +90 -0
  61. package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
  62. package/server/src/Exports/ContactExport.php +12 -5
  63. package/server/src/Exports/DeviceExport.php +92 -0
  64. package/server/src/Exports/DriverExport.php +28 -3
  65. package/server/src/Exports/EquipmentExport.php +94 -0
  66. package/server/src/Exports/FleetExport.php +22 -7
  67. package/server/src/Exports/FuelReportExport.php +16 -1
  68. package/server/src/Exports/IssueExport.php +25 -3
  69. package/server/src/Exports/MaintenanceExport.php +106 -0
  70. package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
  71. package/server/src/Exports/OrderExport.php +24 -5
  72. package/server/src/Exports/PartExport.php +99 -0
  73. package/server/src/Exports/PlaceExport.php +33 -7
  74. package/server/src/Exports/SensorExport.php +94 -0
  75. package/server/src/Exports/ServiceAreaExport.php +31 -7
  76. package/server/src/Exports/ServiceRateExport.php +41 -1
  77. package/server/src/Exports/TelematicExport.php +90 -0
  78. package/server/src/Exports/VehicleExport.php +274 -8
  79. package/server/src/Exports/VendorExport.php +15 -2
  80. package/server/src/Exports/WorkOrderExport.php +92 -0
  81. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
  82. package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
  83. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
  84. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
  85. package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
  86. package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
  87. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
  88. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -0
  89. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
  90. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
  91. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
  92. package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
  93. package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
  94. package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
  95. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
  96. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
  97. package/server/src/Http/Filter/DeviceFilter.php +33 -10
  98. package/server/src/Http/Filter/EquipmentFilter.php +23 -0
  99. package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
  100. package/server/src/Http/Filter/PartFilter.php +47 -0
  101. package/server/src/Http/Filter/SensorFilter.php +34 -6
  102. package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
  103. package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
  104. package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
  105. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
  106. package/server/src/Http/Requests/CreatePartRequest.php +40 -0
  107. package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
  108. package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
  109. package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
  110. package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
  111. package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
  112. package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
  113. package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
  114. package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
  115. package/server/src/Http/Resources/v1/Device.php +72 -0
  116. package/server/src/Http/Resources/v1/Equipment.php +57 -0
  117. package/server/src/Http/Resources/v1/Fleet.php +8 -2
  118. package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
  119. package/server/src/Http/Resources/v1/Part.php +64 -0
  120. package/server/src/Http/Resources/v1/Sensor.php +68 -0
  121. package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
  122. package/server/src/Models/Driver.php +6 -0
  123. package/server/src/Models/Order.php +20 -7
  124. package/server/src/Models/Place.php +11 -3
  125. package/server/src/Observers/PurchaseRateObserver.php +2 -1
  126. package/server/src/Providers/FleetOpsServiceProvider.php +26 -0
  127. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +88 -0
  128. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +108 -0
  129. package/server/src/Support/Ai/Capabilities/ConsoleNavigationCapability.php +55 -0
  130. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +508 -0
  131. package/server/src/Support/Ai/Capabilities/DocsHelpCapability.php +49 -0
  132. package/server/src/Support/Ai/Capabilities/ImportOrdersPreviewCapability.php +66 -0
  133. package/server/src/Support/Ai/Capabilities/OperationalQueryCapability.php +319 -0
  134. package/server/src/Support/Ai/Capabilities/OptimizeOrderRouteCapability.php +240 -0
  135. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +95 -0
  136. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +195 -0
  137. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +150 -0
  138. package/server/src/Support/Analytics/RevenueTrend.php +3 -8
  139. package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
  140. package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
  141. package/server/src/Support/Metrics/EarningsMetric.php +1 -10
  142. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +104 -5
  143. package/server/src/Support/Telematics/Providers/SafeeProvider.php +550 -54
  144. package/server/src/Support/Telematics/TelematicService.php +28 -9
  145. package/server/src/Support/Utils.php +52 -0
  146. package/server/src/routes.php +67 -14
  147. package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
  148. package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
  149. package/server/tests/ExportCoverageTest.php +79 -0
  150. package/server/tests/FleetHierarchyResourceTest.php +12 -0
  151. package/server/tests/MetricsRegistryTest.php +45 -5
  152. package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
  153. package/server/tests/PointResolutionTest.php +63 -0
  154. package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
  155. package/server/tests/TelematicsHardeningTest.php +636 -1
  156. package/addon/components/table/cell/dropdown/action-item.hbs +0 -31
  157. package/addon/components/table/cell/dropdown/action-item.js +0 -62
  158. package/addon/components/table/cell/dropdown.hbs +0 -28
  159. package/addon/components/table/cell/dropdown.js +0 -91
  160. package/app/components/table/cell/dropdown/action-item.js +0 -1
  161. package/app/components/table/cell/dropdown.js +0 -1
@@ -0,0 +1,528 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { inject as service } from '@ember/service';
5
+ import { debug } from '@ember/debug';
6
+
7
+ const FILTER_ALL = 'all';
8
+ const FILTER_DRIVERS = 'drivers';
9
+ const FILTER_VEHICLES = 'vehicles';
10
+ const FILTER_ONLINE = 'online';
11
+
12
+ export default class FleetHierarchyTreeComponent extends Component {
13
+ @service store;
14
+
15
+ @tracked expandedFleetIds = new Set();
16
+ @tracked query = '';
17
+ @tracked activeFilter = FILTER_ALL;
18
+ @tracked loadedDriversByFleetKey = {};
19
+ @tracked loadedVehiclesByFleetKey = {};
20
+ loadingDriverFleetKeys = new Set();
21
+ loadingVehicleFleetKeys = new Set();
22
+
23
+ constructor() {
24
+ super(...arguments);
25
+ this.expandedFleetIds = new Set(this.initialExpandedFleetIds);
26
+ }
27
+
28
+ get filters() {
29
+ return [
30
+ { id: FILTER_ALL, label: 'All' },
31
+ { id: FILTER_DRIVERS, label: 'Drivers' },
32
+ { id: FILTER_VEHICLES, label: 'Vehicles' },
33
+ { id: FILTER_ONLINE, label: 'Online' },
34
+ ];
35
+ }
36
+
37
+ get rootFleet() {
38
+ return this.args.fleet;
39
+ }
40
+
41
+ get fleets() {
42
+ return this.rootFleet ? [this.rootFleet] : [];
43
+ }
44
+
45
+ get normalizedQuery() {
46
+ return String(this.query ?? '')
47
+ .trim()
48
+ .toLowerCase();
49
+ }
50
+
51
+ get hasQuery() {
52
+ return this.normalizedQuery.length > 0;
53
+ }
54
+
55
+ get hasActiveFilter() {
56
+ return this.activeFilter !== FILTER_ALL;
57
+ }
58
+
59
+ get hasControlsApplied() {
60
+ return this.hasQuery || this.hasActiveFilter;
61
+ }
62
+
63
+ get hasHierarchyResources() {
64
+ return this.fleets.some((fleet) => this.fleetHasChildren(fleet));
65
+ }
66
+
67
+ get allFleetNodesExpanded() {
68
+ const fleetKeys = this.collectFleetKeys(this.fleets);
69
+
70
+ return fleetKeys.length > 0 && fleetKeys.every((key) => this.expandedFleetIds.has(key));
71
+ }
72
+
73
+ get expandCollapseIcon() {
74
+ return this.allFleetNodesExpanded ? 'down-left-and-up-right-to-center' : 'up-right-and-down-left-from-center';
75
+ }
76
+
77
+ get expandCollapseHelpText() {
78
+ return this.allFleetNodesExpanded ? 'Collapse all' : 'Expand all';
79
+ }
80
+
81
+ get visibleRows() {
82
+ if (this.hasControlsApplied) {
83
+ return this.buildFilteredFleetRows(this.fleets);
84
+ }
85
+
86
+ return this.buildFleetRows(this.fleets);
87
+ }
88
+
89
+ get emptyTitle() {
90
+ if (this.hasControlsApplied) {
91
+ return 'No matching resources';
92
+ }
93
+
94
+ return 'No fleet resources assigned';
95
+ }
96
+
97
+ get emptyDescription() {
98
+ if (this.hasControlsApplied) {
99
+ return 'Clear the current search or filter to show the full fleet hierarchy.';
100
+ }
101
+
102
+ return 'Assign drivers or vehicles to make this fleet operationally useful.';
103
+ }
104
+
105
+ get initialExpandedFleetIds() {
106
+ return this.fleets.flatMap((fleet) => {
107
+ const key = this.fleetKey(fleet);
108
+ const childKeys = this.fleetSubfleets(fleet)
109
+ .map((subfleet) => this.fleetKey(subfleet))
110
+ .filter(Boolean);
111
+
112
+ return [key, ...childKeys].filter(Boolean);
113
+ });
114
+ }
115
+
116
+ resourceArray(resources) {
117
+ if (!resources) {
118
+ return [];
119
+ }
120
+
121
+ if (typeof resources.toArray === 'function') {
122
+ return resources.toArray();
123
+ }
124
+
125
+ return Array.isArray(resources) ? resources : [];
126
+ }
127
+
128
+ @action displayName(resource) {
129
+ return resource?.displayName ?? resource?.display_name ?? resource?.name ?? resource?.public_id ?? 'Unnamed resource';
130
+ }
131
+
132
+ fleetKey(fleet) {
133
+ return String(fleet?.id ?? fleet?.uuid ?? fleet?.public_id ?? fleet?.name ?? '');
134
+ }
135
+
136
+ fleetSubfleets(fleet) {
137
+ return this.sortByName(this.resourceArray(fleet?.subfleets));
138
+ }
139
+
140
+ @action fleetSubfleetCount(fleet) {
141
+ return this.fleetSubfleets(fleet).length;
142
+ }
143
+
144
+ fleetDrivers(fleet) {
145
+ const embeddedDrivers = this.resourceArray(fleet?.drivers);
146
+
147
+ if (embeddedDrivers.length) {
148
+ return this.sortOnlineFirst(embeddedDrivers);
149
+ }
150
+
151
+ return this.sortOnlineFirst(this.loadedDriversByFleetKey[this.fleetKey(fleet)] ?? []);
152
+ }
153
+
154
+ fleetVehicles(fleet) {
155
+ const embeddedVehicles = this.resourceArray(fleet?.vehicles);
156
+
157
+ if (embeddedVehicles.length) {
158
+ return this.sortOnlineFirst(embeddedVehicles);
159
+ }
160
+
161
+ return this.sortOnlineFirst(this.loadedVehiclesByFleetKey[this.fleetKey(fleet)] ?? []);
162
+ }
163
+
164
+ fleetDriverCount(fleet) {
165
+ return Number(fleet?.drivers_count ?? fleet?.driver_count ?? this.fleetDrivers(fleet).length ?? 0);
166
+ }
167
+
168
+ fleetVehicleCount(fleet) {
169
+ return Number(fleet?.vehicles_count ?? fleet?.vehicle_count ?? this.fleetVehicles(fleet).length ?? 0);
170
+ }
171
+
172
+ fleetOnlineDriverCount(fleet) {
173
+ return Number(fleet?.drivers_online_count ?? this.fleetDrivers(fleet).filter((driver) => driver?.online).length ?? 0);
174
+ }
175
+
176
+ fleetOnlineVehicleCount(fleet) {
177
+ return Number(fleet?.vehicles_online_count ?? this.fleetVehicles(fleet).filter((vehicle) => vehicle?.online).length ?? 0);
178
+ }
179
+
180
+ @action fleetSubtitle(fleet) {
181
+ return `${this.fleetOnlineDriverCount(fleet)}/${this.fleetDriverCount(fleet)} drivers online - ${this.fleetOnlineVehicleCount(fleet)}/${this.fleetVehicleCount(fleet)} vehicles online`;
182
+ }
183
+
184
+ @action driverSubtitle(driver) {
185
+ return [driver?.vehicle_name, driver?.status, driver?.public_id].filter(Boolean).join(' - ') || (driver?.online ? 'Online' : 'Offline');
186
+ }
187
+
188
+ @action vehicleSubtitle(vehicle) {
189
+ return [vehicle?.driver_name, vehicle?.plate_number, vehicle?.vin, vehicle?.status, vehicle?.public_id].filter(Boolean).join(' - ') || (vehicle?.online ? 'Online' : 'Offline');
190
+ }
191
+
192
+ @action fleetHasChildren(fleet) {
193
+ return this.fleetSubfleets(fleet).length > 0 || this.fleetDriverCount(fleet) > 0 || this.fleetVehicleCount(fleet) > 0;
194
+ }
195
+
196
+ @action isFleetExpanded(fleet) {
197
+ return this.expandedFleetIds.has(this.fleetKey(fleet));
198
+ }
199
+
200
+ buildFleetRows(fleets = [], depth = 0) {
201
+ return fleets.flatMap((fleet) => {
202
+ const rows = [this.fleetRow(fleet, depth)];
203
+
204
+ if (!this.isFleetExpanded(fleet)) {
205
+ return rows;
206
+ }
207
+
208
+ rows.push(...this.childRows(fleet, depth + 1));
209
+
210
+ return rows;
211
+ });
212
+ }
213
+
214
+ buildFilteredFleetRows(fleets = [], depth = 0) {
215
+ return fleets.flatMap((fleet) => {
216
+ const fleetMatches = this.fleetMatches(fleet);
217
+ const childDepth = depth + 1;
218
+ const subfleetRows = this.buildFilteredFleetRows(this.fleetSubfleets(fleet), childDepth);
219
+ const driverRows = this.fleetDrivers(fleet)
220
+ .filter((driver) => this.driverVisible(driver))
221
+ .map((driver) => this.driverRow(driver, childDepth));
222
+ const vehicleRows = this.fleetVehicles(fleet)
223
+ .filter((vehicle) => this.vehicleVisible(vehicle))
224
+ .map((vehicle) => this.vehicleRow(vehicle, childDepth));
225
+
226
+ if (fleetMatches) {
227
+ return [this.fleetRow(fleet, depth), ...this.childRows(fleet, childDepth, true)];
228
+ }
229
+
230
+ if (!subfleetRows.length && !driverRows.length && !vehicleRows.length) {
231
+ return [];
232
+ }
233
+
234
+ return [this.fleetRow(fleet, depth), ...subfleetRows, ...driverRows, ...vehicleRows];
235
+ });
236
+ }
237
+
238
+ childRows(fleet, depth, forceExpanded = false) {
239
+ const rows = [];
240
+
241
+ this.fleetSubfleets(fleet).forEach((subfleet) => {
242
+ if (forceExpanded) {
243
+ rows.push(this.fleetRow(subfleet, depth), ...this.childRows(subfleet, depth + 1, true));
244
+ } else {
245
+ rows.push(...this.buildFleetRows([subfleet], depth));
246
+ }
247
+ });
248
+
249
+ if (this.activeFilter !== FILTER_VEHICLES) {
250
+ this.fleetDrivers(fleet)
251
+ .filter((driver) => this.activeFilter !== FILTER_ONLINE || driver?.online)
252
+ .forEach((driver) => rows.push(this.driverRow(driver, depth)));
253
+ }
254
+
255
+ if (this.activeFilter !== FILTER_DRIVERS) {
256
+ this.fleetVehicles(fleet)
257
+ .filter((vehicle) => this.activeFilter !== FILTER_ONLINE || vehicle?.online)
258
+ .forEach((vehicle) => rows.push(this.vehicleRow(vehicle, depth)));
259
+ }
260
+
261
+ return rows;
262
+ }
263
+
264
+ fleetRow(fleet, depth) {
265
+ return {
266
+ type: 'fleet',
267
+ key: `fleet:${this.fleetKey(fleet)}`,
268
+ fleet,
269
+ depth,
270
+ depthClass: this.depthClass(depth),
271
+ };
272
+ }
273
+
274
+ driverRow(driver, depth) {
275
+ return {
276
+ type: 'driver',
277
+ key: `driver:${driver?.id ?? driver?.uuid ?? driver?.public_id ?? this.displayName(driver)}`,
278
+ driver,
279
+ depth,
280
+ depthClass: this.depthClass(depth),
281
+ };
282
+ }
283
+
284
+ vehicleRow(vehicle, depth) {
285
+ return {
286
+ type: 'vehicle',
287
+ key: `vehicle:${vehicle?.id ?? vehicle?.uuid ?? vehicle?.public_id ?? this.displayName(vehicle)}`,
288
+ vehicle,
289
+ depth,
290
+ depthClass: this.depthClass(depth),
291
+ };
292
+ }
293
+
294
+ depthClass(depth) {
295
+ return `fleet-hierarchy-tree-row-depth-${Math.min(Number(depth) || 0, 5)}`;
296
+ }
297
+
298
+ fleetMatches(fleet) {
299
+ return this.resourceMatches(fleet, ['name', 'public_id', 'status', 'slug'], [this.fleetSubtitle(fleet)]);
300
+ }
301
+
302
+ driverMatches(driver) {
303
+ return this.resourceMatches(driver, ['displayName', 'display_name', 'name', 'public_id', 'status', 'vehicle_name'], [this.driverSubtitle(driver)]);
304
+ }
305
+
306
+ vehicleMatches(vehicle) {
307
+ return this.resourceMatches(vehicle, ['displayName', 'display_name', 'name', 'public_id', 'status', 'driver_name', 'plate_number', 'vin'], [this.vehicleSubtitle(vehicle)]);
308
+ }
309
+
310
+ driverVisible(driver) {
311
+ if (this.activeFilter === FILTER_VEHICLES) {
312
+ return false;
313
+ }
314
+
315
+ if (this.activeFilter === FILTER_ONLINE && !driver?.online) {
316
+ return false;
317
+ }
318
+
319
+ return !this.hasQuery || this.driverMatches(driver);
320
+ }
321
+
322
+ vehicleVisible(vehicle) {
323
+ if (this.activeFilter === FILTER_DRIVERS) {
324
+ return false;
325
+ }
326
+
327
+ if (this.activeFilter === FILTER_ONLINE && !vehicle?.online) {
328
+ return false;
329
+ }
330
+
331
+ return !this.hasQuery || this.vehicleMatches(vehicle);
332
+ }
333
+
334
+ resourceMatches(resource, fields = [], extraValues = []) {
335
+ if (!this.hasQuery) {
336
+ return true;
337
+ }
338
+
339
+ const values = [...fields.map((field) => resource?.[field]), ...extraValues];
340
+
341
+ return values.filter(Boolean).some((value) => String(value).toLowerCase().includes(this.normalizedQuery));
342
+ }
343
+
344
+ sortOnlineFirst(resources = []) {
345
+ return [...resources].sort((a, b) => {
346
+ const onlineSort = Number(Boolean(b?.online)) - Number(Boolean(a?.online));
347
+
348
+ if (onlineSort !== 0) {
349
+ return onlineSort;
350
+ }
351
+
352
+ return String(this.displayName(a)).localeCompare(String(this.displayName(b)));
353
+ });
354
+ }
355
+
356
+ sortByName(resources = []) {
357
+ return [...resources].sort((a, b) => String(this.displayName(a)).localeCompare(String(this.displayName(b))));
358
+ }
359
+
360
+ @action toggleFleet(fleet) {
361
+ const key = this.fleetKey(fleet);
362
+ const expandedFleetIds = new Set(this.expandedFleetIds);
363
+ const willExpand = !expandedFleetIds.has(key);
364
+
365
+ if (!willExpand) {
366
+ expandedFleetIds.delete(key);
367
+ } else {
368
+ expandedFleetIds.add(key);
369
+ }
370
+
371
+ this.expandedFleetIds = expandedFleetIds;
372
+
373
+ if (willExpand) {
374
+ this.loadFleetResources(fleet);
375
+ }
376
+ }
377
+
378
+ @action expandAll() {
379
+ this.expandedFleetIds = new Set(this.collectFleetKeys(this.fleets));
380
+ this.loadVisibleFleetResources();
381
+ }
382
+
383
+ @action collapseAll() {
384
+ this.expandedFleetIds = new Set(this.fleets.map((fleet) => this.fleetKey(fleet)).filter(Boolean));
385
+ }
386
+
387
+ @action toggleExpandCollapseAll() {
388
+ if (this.allFleetNodesExpanded) {
389
+ this.collapseAll();
390
+ } else {
391
+ this.expandAll();
392
+ }
393
+ }
394
+
395
+ collectFleetKeys(fleets = []) {
396
+ return fleets.flatMap((fleet) => [this.fleetKey(fleet), ...this.collectFleetKeys(this.fleetSubfleets(fleet))].filter(Boolean));
397
+ }
398
+
399
+ visibleFleets(fleets = this.fleets) {
400
+ return fleets.flatMap((fleet) => {
401
+ if (!this.isFleetExpanded(fleet)) {
402
+ return [fleet];
403
+ }
404
+
405
+ return [fleet, ...this.visibleFleets(this.fleetSubfleets(fleet))];
406
+ });
407
+ }
408
+
409
+ @action loadVisibleFleetResources() {
410
+ this.visibleFleets().forEach((fleet) => this.loadFleetResources(fleet));
411
+ }
412
+
413
+ loadFleetResources(fleet) {
414
+ if (!fleet) {
415
+ return;
416
+ }
417
+
418
+ if (!this.fleetDrivers(fleet).length && this.fleetDriverCount(fleet) > 0) {
419
+ this.loadFleetDrivers(fleet);
420
+ }
421
+
422
+ if (!this.fleetVehicles(fleet).length && this.fleetVehicleCount(fleet) > 0) {
423
+ this.loadFleetVehicles(fleet);
424
+ }
425
+ }
426
+
427
+ async loadFleetDrivers(fleet) {
428
+ const key = this.fleetKey(fleet);
429
+
430
+ if (!key || this.loadingDriverFleetKeys.has(key) || this.loadedDriversByFleetKey[key]) {
431
+ return;
432
+ }
433
+
434
+ this.loadingDriverFleetKeys.add(key);
435
+
436
+ try {
437
+ const drivers = await this.store.query('driver', { fleet: fleet.id, limit: -1 });
438
+ this.loadedDriversByFleetKey = {
439
+ ...this.loadedDriversByFleetKey,
440
+ [key]: this.resourceArray(drivers),
441
+ };
442
+ } catch (error) {
443
+ debug(`Unable to load hierarchy drivers for fleet ${key}: ${error.message}`);
444
+ } finally {
445
+ this.loadingDriverFleetKeys.delete(key);
446
+ }
447
+ }
448
+
449
+ async loadFleetVehicles(fleet) {
450
+ const key = this.fleetKey(fleet);
451
+
452
+ if (!key || this.loadingVehicleFleetKeys.has(key) || this.loadedVehiclesByFleetKey[key]) {
453
+ return;
454
+ }
455
+
456
+ this.loadingVehicleFleetKeys.add(key);
457
+
458
+ try {
459
+ const vehicles = await this.store.query('vehicle', { fleet: fleet.id, limit: -1 });
460
+ this.loadedVehiclesByFleetKey = {
461
+ ...this.loadedVehiclesByFleetKey,
462
+ [key]: this.resourceArray(vehicles),
463
+ };
464
+ } catch (error) {
465
+ debug(`Unable to load hierarchy vehicles for fleet ${key}: ${error.message}`);
466
+ } finally {
467
+ this.loadingVehicleFleetKeys.delete(key);
468
+ }
469
+ }
470
+
471
+ @action updateQuery(event) {
472
+ this.query = event.target.value;
473
+ }
474
+
475
+ @action clearFilters() {
476
+ this.query = '';
477
+ this.activeFilter = FILTER_ALL;
478
+ }
479
+
480
+ @action setFilter(filter) {
481
+ this.activeFilter = filter;
482
+ }
483
+
484
+ @action viewFleet(fleet) {
485
+ if (typeof this.args.onViewFleet === 'function') {
486
+ return this.args.onViewFleet(fleet);
487
+ }
488
+ }
489
+
490
+ @action viewDriver(driver) {
491
+ if (typeof this.args.onViewDriver === 'function') {
492
+ return this.args.onViewDriver(driver);
493
+ }
494
+ }
495
+
496
+ @action viewVehicle(vehicle) {
497
+ if (typeof this.args.onViewVehicle === 'function') {
498
+ return this.args.onViewVehicle(vehicle);
499
+ }
500
+ }
501
+
502
+ @action assignDriver(fleet) {
503
+ if (typeof this.args.onAssignDriver === 'function') {
504
+ return this.args.onAssignDriver(fleet);
505
+ }
506
+ }
507
+
508
+ @action assignVehicle(fleet) {
509
+ if (typeof this.args.onAssignVehicle === 'function') {
510
+ return this.args.onAssignVehicle(fleet);
511
+ }
512
+ }
513
+
514
+ @action calculateDropdownLeftPosition(trigger, content) {
515
+ const triggerRect = trigger.getBoundingClientRect();
516
+ const contentRect = content?.getBoundingClientRect?.();
517
+ const contentWidth = contentRect?.width || 224;
518
+
519
+ return {
520
+ style: {
521
+ position: 'fixed',
522
+ marginTop: '0px',
523
+ left: triggerRect.left - triggerRect.width / 2 - contentWidth - 9,
524
+ top: triggerRect.top - triggerRect.height / 2 + 5,
525
+ },
526
+ };
527
+ }
528
+ }
@@ -63,6 +63,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
63
63
  icon: 'truck',
64
64
  route: 'management',
65
65
  defaultRoute: 'management.index',
66
+ requiresVisibleChildren: true,
66
67
  keywords: ['drivers', 'vehicles', 'fleets', 'contacts', 'places'],
67
68
  children: this.resourcesItems,
68
69
  }),
@@ -72,6 +73,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
72
73
  icon: 'cog',
73
74
  route: 'maintenance',
74
75
  defaultRoute: 'maintenance.index',
76
+ requiresVisibleChildren: true,
75
77
  keywords: ['work orders', 'equipment', 'parts'],
76
78
  children: this.maintenanceItems,
77
79
  }),
@@ -81,6 +83,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
81
83
  icon: 'satellite-dish',
82
84
  route: 'connectivity',
83
85
  defaultRoute: 'connectivity.telematics',
86
+ requiresVisibleChildren: true,
84
87
  keywords: ['telematics', 'fuel integrations', 'devices', 'sensors'],
85
88
  children: this.connectivityItems,
86
89
  }),
@@ -90,6 +93,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
90
93
  icon: 'chart-line',
91
94
  route: 'analytics',
92
95
  defaultRoute: 'analytics.index',
96
+ requiresVisibleChildren: true,
93
97
  keywords: ['reports', 'metrics'],
94
98
  children: this.analyticsItems,
95
99
  }),
@@ -99,6 +103,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
99
103
  icon: 'gear',
100
104
  route: 'settings',
101
105
  defaultRoute: 'settings.index',
106
+ requiresVisibleChildren: true,
102
107
  keywords: ['configuration', 'navigator', 'map', 'routing', 'notifications'],
103
108
  children: this.settingsItems,
104
109
  }),
@@ -164,7 +169,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
164
169
 
165
170
  get connectivityItems() {
166
171
  return this.withRegistryItems('connectivity', [
167
- this.createItem('menu.telematics', 'satellite-dish', 'connectivity.telematics', 'fleet-ops list telematic', 'fleet-ops see telematic', ['connectivity hub']),
172
+ this.createHubItem(this.intl.t('menu.telematics'), 'satellite-dish', 'connectivity.telematics', 'fleet-ops list telematic', 'fleet-ops see telematic', ['connectivity hub']),
168
173
  this.createItem('menu.fuel-providers', 'gas-pump', 'connectivity.fuel-providers', 'fleet-ops list fuel-report', 'fleet-ops see fuel-report', ['fuel integrations']),
169
174
  this.createItem('menu.devices', 'hard-drive', 'connectivity.devices', 'fleet-ops list device', 'fleet-ops see device'),
170
175
  this.createItem('menu.sensors', 'temperature-full', 'connectivity.sensors', 'fleet-ops list sensor', 'fleet-ops see sensor'),
@@ -247,13 +252,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
247
252
  this.universeSettingsMenuItems = registeredMenuItems.filter((menuItem) => menuItem.section === 'settings');
248
253
  }
249
254
 
250
- createBranch({ id, label, icon, route, defaultRoute, children, keywords = [] }) {
255
+ createBranch({ id, label, icon, route, defaultRoute, requiresVisibleChildren = false, children, keywords = [] }) {
251
256
  return {
252
257
  id,
253
258
  label,
254
259
  icon,
255
260
  route: this.fullRoute(route),
256
261
  defaultRoute: this.fullRoute(defaultRoute),
262
+ requiresVisibleChildren,
257
263
  children: children.filter((item) => item.visible !== false),
258
264
  keywords,
259
265
  };
@@ -267,12 +273,12 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
267
273
  icon,
268
274
  route: this.fullRoute(route),
269
275
  permission,
270
- visible: this.abilities.can(ability),
276
+ visiblePermission: ability,
271
277
  keywords: [intl, route, ...keywords].filter(Boolean),
272
278
  };
273
279
  }
274
280
 
275
- createHubItem(label, icon, route, permission, ability, keywords = []) {
281
+ createHubItem(label, icon, route, _permission, _ability, keywords = []) {
276
282
  return {
277
283
  pinnedFirst: true,
278
284
  priority: this.defaultPriorityForRoute(route),
@@ -280,8 +286,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
280
286
  description: label,
281
287
  icon,
282
288
  route: this.fullRoute(route),
283
- permission,
284
- visible: this.abilities.can(ability),
289
+ isNavigationHub: true,
285
290
  keywords: [label, route, ...keywords].filter(Boolean),
286
291
  };
287
292
  }
@@ -390,10 +395,11 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
390
395
  }
391
396
  }
392
397
 
393
- @action shouldSyncInitialActiveParent({ activePath = [], routeName }) {
398
+ @action shouldSyncInitialActiveParent({ activePath = [], currentURL }) {
394
399
  const [parent, child] = activePath;
395
- const defaultOrdersRoutes = [this.fullRoute('operations.orders'), this.fullRoute('operations.orders.index')];
396
- const isDefaultOrdersLanding = parent?.id === 'operations' && child?.route === this.fullRoute('operations.orders') && defaultOrdersRoutes.includes(routeName);
400
+ const normalizedURL = (currentURL ?? '').split('?')[0].replace(/\/+$/, '') || '/';
401
+ const isFleetOpsRootURL = normalizedURL === '/fleet-ops';
402
+ const isDefaultOrdersLanding = parent?.id === 'operations' && child?.route === this.fullRoute('operations.orders') && isFleetOpsRootURL;
397
403
 
398
404
  return !isDefaultOrdersLanding;
399
405
  }
@@ -116,6 +116,7 @@ export default class MapDrawerDeviceEventListingComponent extends Component {
116
116
  ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.device-event') }),
117
117
  cellClassNames: 'overflow-visible',
118
118
  wrapperClass: 'flex items-center justify-end mx-2',
119
+ renderInPlace: false,
119
120
  calculatePosition: calculateMapDrawerDropdownPosition,
120
121
  width: '10%',
121
122
  actions: [
@@ -72,6 +72,7 @@ export default class MapDrawerDriverListingComponent extends Component {
72
72
  ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.driver') }),
73
73
  cellClassNames: 'overflow-visible',
74
74
  wrapperClass: 'flex items-center justify-end mx-2',
75
+ renderInPlace: false,
75
76
  calculatePosition: calculateMapDrawerDropdownPosition,
76
77
  width: '90px',
77
78
  actions: [
@@ -54,6 +54,7 @@ export default class MapDrawerPlaceListingComponent extends Component {
54
54
  ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.place') }),
55
55
  cellClassNames: 'overflow-visible',
56
56
  wrapperClass: 'flex items-center justify-end mx-2',
57
+ renderInPlace: false,
57
58
  calculatePosition: calculateMapDrawerDropdownPosition,
58
59
  width: '90px',
59
60
  actions: [
@@ -66,6 +66,7 @@ export default class MapDrawerVehicleListingComponent extends Component {
66
66
  ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.vehicle') }),
67
67
  cellClassNames: 'overflow-visible',
68
68
  wrapperClass: 'flex items-center justify-end mx-2',
69
+ renderInPlace: false,
69
70
  calculatePosition: calculateMapDrawerDropdownPosition,
70
71
  width: '90px',
71
72
  actions: [