@fleetbase/fleetops-engine 0.6.53 → 0.6.55

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 (212) hide show
  1. package/addon/components/admin/routing-settings.hbs +2 -1
  2. package/addon/components/cell/attached-vehicle.hbs +10 -0
  3. package/addon/components/cell/attached-vehicle.js +58 -0
  4. package/addon/components/cell/device-identity.hbs +24 -0
  5. package/addon/components/cell/device-identity.js +129 -0
  6. package/addon/components/cell/driver-identity.hbs +33 -0
  7. package/addon/components/cell/driver-identity.js +143 -0
  8. package/addon/components/cell/equipment-identity.hbs +5 -0
  9. package/addon/components/cell/equipment-identity.js +48 -0
  10. package/addon/components/cell/part-identity.hbs +5 -0
  11. package/addon/components/cell/part-identity.js +63 -0
  12. package/addon/components/cell/telematic-device.hbs +38 -0
  13. package/addon/components/cell/telematic-device.js +44 -0
  14. package/addon/components/cell/telematic-provider.hbs +33 -16
  15. package/addon/components/cell/telematic-provider.js +50 -8
  16. package/addon/components/cell/vehicle-identity.hbs +33 -0
  17. package/addon/components/cell/vehicle-identity.js +138 -0
  18. package/addon/components/device/details.hbs +106 -135
  19. package/addon/components/device/details.js +174 -1
  20. package/addon/components/device/form.hbs +2 -2
  21. package/addon/components/device/form.js +7 -0
  22. package/addon/components/device/panel-header.hbs +46 -21
  23. package/addon/components/device/panel-header.js +49 -1
  24. package/addon/components/device/panel-tabs/events.hbs +30 -0
  25. package/addon/components/device/panel-tabs/events.js +128 -0
  26. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  27. package/addon/components/device/panel-tabs/sensors.js +93 -0
  28. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  29. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  30. package/addon/components/equipment/form.js +8 -1
  31. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +76 -11
  32. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  33. package/addon/components/layout/fleet-ops-sidebar.js +101 -17
  34. package/addon/components/map/drawer/device-event-listing.js +2 -0
  35. package/addon/components/map/drawer/driver-listing.js +2 -0
  36. package/addon/components/map/drawer/place-listing.js +2 -0
  37. package/addon/components/map/drawer/position-listing.hbs +11 -6
  38. package/addon/components/map/drawer/position-listing.js +17 -4
  39. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  40. package/addon/components/order/form/details.hbs +1 -43
  41. package/addon/components/order/form/details.js +18 -53
  42. package/addon/components/order/form/orchestrator-constraints.hbs +40 -0
  43. package/addon/components/order/form/orchestrator-constraints.js +61 -0
  44. package/addon/components/order/form/payload.js +48 -2
  45. package/addon/components/order/form/route.js +15 -0
  46. package/addon/components/order/form/service-rate.hbs +62 -11
  47. package/addon/components/order/form/service-rate.js +124 -2
  48. package/addon/components/order/form.hbs +2 -0
  49. package/addon/components/sensor/panel-header.hbs +47 -15
  50. package/addon/components/sensor/panel-header.js +59 -1
  51. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  52. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  53. package/addon/components/table/cell/dropdown.hbs +28 -0
  54. package/addon/components/table/cell/dropdown.js +91 -0
  55. package/addon/components/telematic/details.hbs +7 -1
  56. package/addon/components/telematic/form.js +16 -8
  57. package/addon/components/vendor/panel-header.hbs +82 -0
  58. package/addon/components/vendor/panel-header.js +48 -0
  59. package/addon/components/work-order/details.hbs +5 -1
  60. package/addon/components/work-order/form.hbs +26 -4
  61. package/addon/components/work-order/form.js +5 -1
  62. package/addon/controllers/connectivity/devices/index/details/events.js +244 -1
  63. package/addon/controllers/connectivity/devices/index/details/sensors.js +199 -0
  64. package/addon/controllers/connectivity/devices/index/details/vehicle.js +122 -0
  65. package/addon/controllers/connectivity/devices/index/details.js +12 -0
  66. package/addon/controllers/connectivity/devices/index.js +144 -129
  67. package/addon/controllers/connectivity/events/details.js +3 -0
  68. package/addon/controllers/connectivity/events/index.js +187 -11
  69. package/addon/controllers/connectivity/sensors/index.js +156 -14
  70. package/addon/controllers/connectivity/telematics/details/attachments.js +220 -54
  71. package/addon/controllers/connectivity/telematics/details/devices.js +170 -129
  72. package/addon/controllers/connectivity/telematics/details/events.js +39 -11
  73. package/addon/controllers/connectivity/telematics/details/sensors.js +39 -19
  74. package/addon/controllers/maintenance/equipment/index.js +1 -2
  75. package/addon/controllers/maintenance/parts/index.js +1 -2
  76. package/addon/controllers/maintenance/work-orders/index.js +18 -1
  77. package/addon/controllers/management/drivers/index.js +82 -42
  78. package/addon/controllers/management/fuel-reports/index.js +63 -4
  79. package/addon/controllers/management/issues/index.js +63 -4
  80. package/addon/controllers/management/vehicles/index.js +33 -6
  81. package/addon/routes/connectivity/devices/index/details/events.js +37 -1
  82. package/addon/routes/connectivity/devices/index/details/sensors.js +35 -0
  83. package/addon/routes/connectivity/devices/index/details/vehicle.js +29 -0
  84. package/addon/routes/connectivity/devices/index.js +7 -0
  85. package/addon/routes/connectivity/events/details.js +3 -0
  86. package/addon/routes/connectivity/events/index.js +6 -2
  87. package/addon/routes/connectivity/sensors/index.js +7 -2
  88. package/addon/routes/connectivity/telematics/details/attachments.js +72 -9
  89. package/addon/routes/connectivity/telematics/details/devices.js +6 -0
  90. package/addon/routes/connectivity/telematics/details/sensors.js +1 -0
  91. package/addon/routes/connectivity/telematics/details.js +1 -1
  92. package/addon/routes/connectivity/telematics/edit.js +1 -1
  93. package/addon/routes/maintenance/work-orders/index.js +1 -0
  94. package/addon/routes.js +2 -0
  95. package/addon/services/device-actions.js +30 -0
  96. package/addon/services/device-event-actions.js +1 -1
  97. package/addon/services/equipment-actions.js +1 -0
  98. package/addon/services/order-creation.js +54 -1
  99. package/addon/services/order-validation.js +3 -2
  100. package/addon/services/sensor-actions.js +3 -0
  101. package/addon/services/service-rate-actions.js +40 -1
  102. package/addon/services/vehicle-actions.js +24 -12
  103. package/addon/services/vendor-actions.js +3 -0
  104. package/addon/styles/fleetops-engine.css +28 -4
  105. package/addon/templates/connectivity/devices/index/details/events.hbs +85 -1
  106. package/addon/templates/connectivity/devices/index/details/sensors.hbs +73 -0
  107. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +147 -0
  108. package/addon/templates/connectivity/telematics/details/attachments.hbs +198 -121
  109. package/addon/templates/connectivity/telematics/details/devices.hbs +4 -1
  110. package/addon/templates/connectivity/telematics/details/events.hbs +4 -1
  111. package/addon/templates/connectivity/telematics/details/sensors.hbs +3 -0
  112. package/addon/templates/management/vehicles/index/edit.hbs +1 -1
  113. package/addon/templates/management/vendors/index/details.hbs +2 -2
  114. package/addon/templates/settings/notifications.hbs +2 -1
  115. package/addon/templates/settings/routing.hbs +1 -0
  116. package/addon/utils/device-table-columns.js +288 -0
  117. package/addon/utils/fleet-ops-options.js +38 -0
  118. package/addon/utils/identity-cell-resource.js +20 -0
  119. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  120. package/addon/utils/trackable-option.js +93 -0
  121. package/app/components/cell/attached-vehicle.js +1 -0
  122. package/app/components/cell/device-identity.js +1 -0
  123. package/app/components/cell/driver-identity.js +1 -0
  124. package/app/components/cell/equipment-identity.js +1 -0
  125. package/app/components/cell/part-identity.js +1 -0
  126. package/app/components/cell/telematic-device.js +1 -0
  127. package/app/components/cell/vehicle-identity.js +1 -0
  128. package/app/components/device/panel-tabs/events.hbs +30 -0
  129. package/app/components/device/panel-tabs/events.js +128 -0
  130. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  131. package/app/components/device/panel-tabs/sensors.js +93 -0
  132. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  133. package/app/components/device/panel-tabs/vehicle.js +90 -0
  134. package/app/components/order/form/orchestrator-constraints.js +1 -0
  135. package/app/components/table/cell/dropdown/action-item.js +1 -0
  136. package/app/components/table/cell/dropdown.js +1 -0
  137. package/app/components/vendor/panel-header.js +1 -0
  138. package/app/controllers/connectivity/devices/index/details/sensors.js +1 -0
  139. package/app/controllers/connectivity/devices/index/details/vehicle.js +1 -0
  140. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  141. package/app/routes/connectivity/devices/index/details/sensors.js +1 -0
  142. package/app/routes/connectivity/devices/index/details/vehicle.js +1 -0
  143. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  144. package/app/templates/connectivity/devices/index/details/sensors.js +1 -0
  145. package/app/templates/connectivity/devices/index/details/vehicle.js +1 -0
  146. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  147. package/app/utils/map-drawer-dropdown-position.js +1 -0
  148. package/app/utils/trackable-option.js +1 -0
  149. package/composer.json +1 -1
  150. package/extension.json +1 -1
  151. package/package.json +4 -4
  152. package/server/migrations/2025_08_28_054930_create_work_orders_table.php +1 -0
  153. package/server/migrations/2026_06_18_000001_add_category_to_work_orders_table.php +32 -0
  154. package/server/seeders/Testing/MaintenanceSeeder.php +5 -4
  155. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +1 -0
  156. package/server/src/Console/Commands/SyncTelematics.php +89 -0
  157. package/server/src/Exceptions/TelematicProviderException.php +13 -2
  158. package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -21
  159. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +55 -1
  160. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +17 -6
  161. package/server/src/Http/Controllers/Internal/v1/FleetController.php +9 -0
  162. package/server/src/Http/Controllers/Internal/v1/LiveController.php +178 -1
  163. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +1 -0
  164. package/server/src/Http/Controllers/Internal/v1/OrderController.php +4 -32
  165. package/server/src/Http/Controllers/Internal/v1/SearchController.php +2 -2
  166. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +4 -4
  167. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  168. package/server/src/Http/Filter/DeviceEventFilter.php +65 -0
  169. package/server/src/Http/Filter/DeviceFilter.php +80 -0
  170. package/server/src/Http/Filter/SensorFilter.php +125 -0
  171. package/server/src/Http/Filter/VehicleFilter.php +13 -0
  172. package/server/src/Http/Requests/CreateDriverRequest.php +14 -13
  173. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -0
  174. package/server/src/Http/Resources/v1/Driver.php +11 -2
  175. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  176. package/server/src/Http/Resources/v1/WorkOrder.php +1 -0
  177. package/server/src/Jobs/FinalizeApiOrderCreation.php +47 -0
  178. package/server/src/Jobs/FinalizeInternalOrderCreation.php +35 -0
  179. package/server/src/Jobs/NotifyBulkAssignedDriver.php +47 -0
  180. package/server/src/Jobs/SyncTelematicDevicesJob.php +180 -123
  181. package/server/src/Models/Device.php +17 -1
  182. package/server/src/Models/DeviceEvent.php +82 -1
  183. package/server/src/Models/Driver.php +16 -0
  184. package/server/src/Models/Equipment.php +55 -0
  185. package/server/src/Models/Sensor.php +15 -2
  186. package/server/src/Models/Telematic.php +1 -1
  187. package/server/src/Models/Vehicle.php +5 -1
  188. package/server/src/Models/WorkOrder.php +5 -2
  189. package/server/src/Observers/DriverObserver.php +3 -3
  190. package/server/src/Observers/FleetObserver.php +23 -0
  191. package/server/src/Observers/VehicleObserver.php +3 -3
  192. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  193. package/server/src/Support/LiveCacheService.php +1 -1
  194. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +143 -20
  195. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  196. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  197. package/server/src/Support/Telematics/Providers/SafeeProvider.php +70 -5
  198. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  199. package/server/src/Support/Telematics/TelematicService.php +330 -30
  200. package/server/src/routes.php +1 -1
  201. package/server/tests/DeviceFilterTest.php +45 -0
  202. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  203. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  204. package/server/tests/OperationsMonitorEndpointTest.php +38 -0
  205. package/server/tests/OrderQueuedClosureRegressionTest.php +36 -0
  206. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  207. package/server/tests/TelematicsHardeningTest.php +598 -6
  208. package/server/tests/VehicleDriverAssignmentTest.php +27 -0
  209. package/translations/en-us.yaml +1 -0
  210. package/addon/controllers/connectivity/events/index/details.js +0 -3
  211. package/addon/routes/connectivity/events/index/details.js +0 -3
  212. /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
@@ -0,0 +1,68 @@
1
+ <div class="flex flex-col gap-3 p-3" ...attributes>
2
+ <div class="flex items-start justify-between gap-3">
3
+ <div class="min-w-0">
4
+ <h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Vehicle Attachment</h2>
5
+ <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">Fleet context for telemetry from this device.</p>
6
+ </div>
7
+ <div class="flex flex-wrap items-center justify-end gap-2">
8
+ {{#if this.hasVehicle}}
9
+ {{#if this.canOpenVehicle}}
10
+ <Button @icon="eye" @text="Open Vehicle" @size="xs" @onClick={{this.openVehicle}} />
11
+ {{/if}}
12
+ {{#if this.canLocateVehicle}}
13
+ <Button @icon="location-dot" @text="Locate" @size="xs" @onClick={{this.locateVehicle}} />
14
+ {{/if}}
15
+ <Button @icon="shuffle" @text="Change Vehicle" @size="xs" @onClick={{this.attachToVehicle}} />
16
+ <Button @icon="unlink" @text="Detach" @size="xs" @type="danger" @onClick={{this.detachFromVehicle}} />
17
+ {{else}}
18
+ <Button @icon="link" @text="Attach Vehicle" @size="xs" @onClick={{this.attachToVehicle}} />
19
+ {{/if}}
20
+ </div>
21
+ </div>
22
+
23
+ {{#if this.hasVehicle}}
24
+ <section class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
25
+ <div class="flex gap-3 p-3">
26
+ <Image
27
+ src={{this.vehiclePhotoUrl}}
28
+ @fallbackSrc={{config "defaultValues.vehicleAvatar"}}
29
+ alt={{this.vehicleName}}
30
+ class="h-16 w-16 flex-shrink-0 rounded-md object-cover shadow-sm"
31
+ />
32
+ <div class="min-w-0 flex-1">
33
+ <div class="flex flex-wrap items-center gap-2">
34
+ <h3 class="truncate text-base font-bold text-gray-900 dark:text-gray-100">{{this.vehicleName}}</h3>
35
+ {{#if this.vehicleStatus}}
36
+ <Badge @status={{this.vehicleStatus}} @size="xs">{{smart-humanize this.vehicleStatus}}</Badge>
37
+ {{/if}}
38
+ </div>
39
+ <div class="mt-1 text-xs text-gray-500 dark:text-gray-300">{{n-a this.vehicleSubtitle}}</div>
40
+ <div class="mt-3 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3">
41
+ <div>
42
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Driver</div>
43
+ <div class="truncate text-gray-800 dark:text-gray-100">{{n-a this.vehicleDriverName}}</div>
44
+ </div>
45
+ <div>
46
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Attached Device</div>
47
+ <div class="truncate text-gray-800 dark:text-gray-100">{{n-a this.device.displayName this.device.name}}</div>
48
+ </div>
49
+ <div>
50
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Device Last Seen</div>
51
+ <div class="truncate text-gray-800 dark:text-gray-100">{{n-a (format-date-fns this.device.last_online_at "dd MMM yyyy, HH:mm")}}</div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </section>
57
+ {{else}}
58
+ <Telematic::TabEmptyState
59
+ @tone="warning"
60
+ @icon="link-slash"
61
+ @title="No vehicle attached"
62
+ @message="Attach this device to a vehicle so telemetry has fleet context."
63
+ @primaryText="Attach Vehicle"
64
+ @primaryIcon="link"
65
+ @primaryAction={{this.attachToVehicle}}
66
+ />
67
+ {{/if}}
68
+ </div>
@@ -0,0 +1,90 @@
1
+ import Component from '@glimmer/component';
2
+ import { action } from '@ember/object';
3
+ import { inject as service } from '@ember/service';
4
+
5
+ export default class DevicePanelTabsVehicleComponent extends Component {
6
+ @service deviceActions;
7
+ @service hostRouter;
8
+ @service mapManager;
9
+ @service vehicleActions;
10
+
11
+ get device() {
12
+ return this.args.resource ?? this.args.model;
13
+ }
14
+
15
+ get vehicle() {
16
+ return this.device?.attachable;
17
+ }
18
+
19
+ get vehicleName() {
20
+ return this.device?.attached_to_name ?? this.vehicle?.displayName ?? this.vehicle?.display_name ?? this.vehicle?.name;
21
+ }
22
+
23
+ get vehicleSubtitle() {
24
+ return this.vehicle?.plate_number ?? this.vehicle?.call_sign ?? this.vehicle?.vin ?? this.vehicle?.public_id ?? this.device?.attachable_uuid;
25
+ }
26
+
27
+ get vehiclePhotoUrl() {
28
+ return this.vehicle?.photo_url ?? this.vehicle?.avatar_url;
29
+ }
30
+
31
+ get vehicleStatus() {
32
+ return this.vehicle?.status ?? (this.vehicle?.online ? 'online' : null);
33
+ }
34
+
35
+ get vehicleDriverName() {
36
+ return this.vehicle?.driver?.displayName ?? this.vehicle?.driver?.display_name ?? this.vehicle?.driver?.name ?? this.vehicle?.driver_name;
37
+ }
38
+
39
+ get hasVehicle() {
40
+ return Boolean(this.vehicleName || this.device?.attachable_uuid);
41
+ }
42
+
43
+ get canOpenVehicle() {
44
+ return Boolean(this.vehicle?.id);
45
+ }
46
+
47
+ get canLocateVehicle() {
48
+ return Boolean(this.vehicle?.id && (this.vehicle?.location || this.vehicle?.last_position));
49
+ }
50
+
51
+ @action attachToVehicle() {
52
+ return this.deviceActions.attachToVehicle(this.device, { callback: () => this.device?.reload?.() });
53
+ }
54
+
55
+ @action detachFromVehicle() {
56
+ return this.deviceActions.detachFromVehicle(this.device, { callback: () => this.device?.reload?.() });
57
+ }
58
+
59
+ @action openVehicle() {
60
+ if (this.vehicle?.id) {
61
+ return this.vehicleActions.panel?.view
62
+ ? this.vehicleActions.panel.view(this.vehicle)
63
+ : this.hostRouter.transitionTo('console.fleet-ops.management.vehicles.index.details', this.vehicle);
64
+ }
65
+ }
66
+
67
+ @action async locateVehicle() {
68
+ if (!this.vehicle?.id) {
69
+ return;
70
+ }
71
+
72
+ await this.transitionToLiveMap();
73
+ await this.mapManager.waitForMap({ timeoutMs: 8000 });
74
+
75
+ this.mapManager.focusResource(this.vehicle, 16, {
76
+ paddingBottomRight: [300, 200],
77
+ moveend: () => {
78
+ this.vehicleActions.panel?.view?.(this.vehicle, { closeOnTransition: true });
79
+ },
80
+ });
81
+ }
82
+
83
+ async transitionToLiveMap() {
84
+ try {
85
+ await this.hostRouter.transitionTo('console.fleet-ops.operations.orders.index', { queryParams: { layout: 'map' } });
86
+ } catch (_) {
87
+ // Keep locate usable if another transition is already active.
88
+ }
89
+ }
90
+ }
@@ -11,9 +11,16 @@ import { task } from 'ember-concurrency';
11
11
  const TYPE_TO_MODEL = {
12
12
  'fleet-ops:vehicle': 'vehicle',
13
13
  'fleet-ops:driver': 'driver',
14
+ 'Fleetbase\\FleetOps\\Models\\Vehicle': 'vehicle',
15
+ 'Fleetbase\\FleetOps\\Models\\Driver': 'driver',
14
16
  'fleet-ops:equipment': 'equipment',
15
17
  };
16
18
 
19
+ const TYPE_TO_OPTION_VALUE = {
20
+ 'Fleetbase\\FleetOps\\Models\\Vehicle': 'fleet-ops:vehicle',
21
+ 'Fleetbase\\FleetOps\\Models\\Driver': 'fleet-ops:driver',
22
+ };
23
+
17
24
  export default class EquipmentFormComponent extends Component {
18
25
  @service fetch;
19
26
  @service currentUser;
@@ -45,7 +52,7 @@ export default class EquipmentFormComponent extends Component {
45
52
  const { resource } = args;
46
53
  if (resource?.equipable_type) {
47
54
  this.equipableModelName = TYPE_TO_MODEL[resource.equipable_type] ?? null;
48
- this.selectedEquipableType = this.equipableTypeOptions.find((o) => o.value === resource.equipable_type) ?? null;
55
+ this.selectedEquipableType = this.equipableTypeOptions.find((o) => o.value === (TYPE_TO_OPTION_VALUE[resource.equipable_type] ?? resource.equipable_type)) ?? null;
49
56
  }
50
57
  }
51
58
 
@@ -7,6 +7,7 @@ import { task } from 'ember-concurrency';
7
7
 
8
8
  export default class LayoutFleetOpsSidebarOperationsMonitorComponent extends Component {
9
9
  @service store;
10
+ @service fetch;
10
11
  @service universe;
11
12
  @service hostRouter;
12
13
  @service mapManager;
@@ -203,11 +204,11 @@ export default class LayoutFleetOpsSidebarOperationsMonitorComponent extends Com
203
204
  }
204
205
 
205
206
  fleetDrivers(fleet) {
206
- return this.resourceArray(fleet?.drivers);
207
+ return this.resolveFleetResources(fleet, 'driver');
207
208
  }
208
209
 
209
210
  fleetVehicles(fleet) {
210
- return this.resourceArray(fleet?.vehicles);
211
+ return this.resolveFleetResources(fleet, 'vehicle');
211
212
  }
212
213
 
213
214
  fleetSubfleets(fleet) {
@@ -339,6 +340,53 @@ export default class LayoutFleetOpsSidebarOperationsMonitorComponent extends Com
339
340
  });
340
341
  }
341
342
 
343
+ get driverById() {
344
+ return this.resourcesById(this.fallbackDrivers);
345
+ }
346
+
347
+ get vehicleById() {
348
+ return this.resourcesById(this.fallbackVehicles);
349
+ }
350
+
351
+ resourcesById(resources = []) {
352
+ return this.resourceArray(resources).reduce((map, resource) => {
353
+ this.resourceIdentifiers(resource).forEach((id) => map.set(id, resource));
354
+
355
+ return map;
356
+ }, new Map());
357
+ }
358
+
359
+ resourceIdentifiers(resource) {
360
+ return [resource?.id, resource?.uuid, resource?.public_id].filter(Boolean).map((id) => String(id));
361
+ }
362
+
363
+ resolveFleetResources(fleet, type) {
364
+ const embeddedResources = this.resourceArray(fleet?.[`${type}s`]);
365
+
366
+ if (embeddedResources.length) {
367
+ return embeddedResources;
368
+ }
369
+
370
+ const ids = this.resourceArray(fleet?.[`${type}_ids`]);
371
+ const resources = type === 'driver' ? this.driverById : this.vehicleById;
372
+
373
+ return ids.map((id) => resources.get(String(id))).filter(Boolean);
374
+ }
375
+
376
+ normalizeMonitorResource(type, resource) {
377
+ if (!resource) {
378
+ return resource;
379
+ }
380
+
381
+ try {
382
+ this.store.pushPayload?.(type, { [type]: resource });
383
+ } catch (_) {
384
+ // Keep the monitor usable in tests and older hosts without pushPayload support.
385
+ }
386
+
387
+ return this.store.peekRecord?.(type, resource.id) ?? this.store.peekRecord?.(type, resource.public_id) ?? resource;
388
+ }
389
+
342
390
  listenForChanges() {
343
391
  this.universe.on('fleet-ops.driver.saved', () => this.loadFallbackResources.perform());
344
392
  this.universe.on('fleet-ops.vehicle.saved', () => this.loadFallbackResources.perform());
@@ -572,18 +620,35 @@ export default class LayoutFleetOpsSidebarOperationsMonitorComponent extends Com
572
620
 
573
621
  @task *loadFallbackResources() {
574
622
  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;
623
+ const response = yield this.fetch.get('fleet-ops/live/operations-monitor', {}, { namespace: 'int/v1' });
624
+ const drivers = this.resourceArray(response?.drivers).map((driver) => this.normalizeMonitorResource('driver', driver));
625
+ const vehicles = this.resourceArray(response?.vehicles).map((vehicle) => this.normalizeMonitorResource('vehicle', vehicle));
626
+ const fleets = this.resourceArray(response?.fleets).map((fleet) => this.normalizeMonitorFleet(fleet));
627
+
628
+ this.fallbackDrivers = drivers;
629
+ this.fallbackVehicles = vehicles;
630
+ this.fallbackFleets = fleets;
584
631
  this.expandedFleetIds = new Set(this.collectFleetKeys(this.fallbackFleets));
585
632
  } catch (error) {
586
633
  this.notifications.serverError(error);
587
634
  }
588
635
  }
636
+
637
+ normalizeMonitorFleet(fleet) {
638
+ const normalizedFleet = this.normalizeMonitorResource('fleet', fleet);
639
+ const subfleets = this.resourceArray(fleet?.subfleets).map((subfleet) => this.normalizeMonitorFleet(subfleet));
640
+
641
+ if (normalizedFleet?.set) {
642
+ normalizedFleet.set('driver_ids', this.resourceArray(fleet?.driver_ids));
643
+ normalizedFleet.set('vehicle_ids', this.resourceArray(fleet?.vehicle_ids));
644
+ normalizedFleet.set('subfleets', subfleets);
645
+
646
+ return normalizedFleet;
647
+ }
648
+
649
+ return {
650
+ ...fleet,
651
+ subfleets,
652
+ };
653
+ }
589
654
  }
@@ -3,7 +3,7 @@
3
3
  @primaryAction={{this.createOrderAction}}
4
4
  @searchPlaceholder="Search Fleet-Ops..."
5
5
  @searchProvider={{this.searchNavigation}}
6
- @initialActiveParentSync={{false}}
6
+ @shouldSyncInitialActiveParent={{this.shouldSyncInitialActiveParent}}
7
7
  class="fleet-ops-sidebar-navigator"
8
8
  >
9
9
  <:footer as |state|>
@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { action } from '@ember/object';
5
+ import isMenuItemActive from '@fleetbase/ember-ui/utils/is-menu-item-active';
5
6
 
6
7
  const SECTION_REGISTRY_KEYS = {
7
8
  operations: 'universeOperationsMenuItems',
@@ -198,23 +199,26 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
198
199
  }
199
200
 
200
201
  get registryRootItems() {
201
- return this.universeMenuItems.filter((item) => !item.renderComponentInPlace).map((item) => this.registryItem(item));
202
+ return this.sortByPriority(this.universeMenuItems.filter((item) => !item.renderComponentInPlace).map((item) => this.registryItem(item)));
202
203
  }
203
204
 
204
205
  get registryPanelItems() {
205
- return this.universeMenuPanels.map((panel) => {
206
- const children = (panel.items ?? []).filter((item) => !item.renderComponentInPlace).map((item) => this.registryItem(item));
207
-
208
- return {
209
- id: panel.id ?? panel.slug ?? panel.title,
210
- label: panel.intl ? this.intl.t(panel.intl) : panel.title,
211
- icon: panel.icon,
212
- visible: panel.visible,
213
- permission: panel.permission,
214
- keywords: [panel.slug, panel.title, panel.intl].filter(Boolean),
215
- children,
216
- };
217
- });
206
+ return this.sortByPriority(
207
+ this.universeMenuPanels.map((panel) => {
208
+ const children = this.sortByPriority((panel.items ?? []).filter((item) => !item.renderComponentInPlace).map((item) => this.registryItem(item)));
209
+
210
+ return {
211
+ id: panel.id ?? panel.slug ?? panel.title,
212
+ label: panel.intl ? this.intl.t(panel.intl) : panel.title,
213
+ icon: panel.icon,
214
+ visible: panel.visible,
215
+ permission: panel.permission,
216
+ priority: panel.priority,
217
+ keywords: [panel.slug, panel.title, panel.intl].filter(Boolean),
218
+ children,
219
+ };
220
+ })
221
+ );
218
222
  }
219
223
 
220
224
  get footerRegistryComponents() {
@@ -257,6 +261,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
257
261
 
258
262
  createItem(intl, icon, route, permission, ability, keywords = []) {
259
263
  return {
264
+ priority: this.defaultPriorityForRoute(route),
260
265
  label: this.intl.t(intl),
261
266
  description: this.intl.t(intl),
262
267
  icon,
@@ -269,6 +274,8 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
269
274
 
270
275
  createHubItem(label, icon, route, permission, ability, keywords = []) {
271
276
  return {
277
+ pinnedFirst: true,
278
+ priority: this.defaultPriorityForRoute(route),
272
279
  label,
273
280
  description: label,
274
281
  icon,
@@ -280,8 +287,9 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
280
287
  }
281
288
 
282
289
  registryItem(menuItem) {
283
- return {
290
+ const registryMenuItem = {
284
291
  ...menuItem,
292
+ _virtual: true,
285
293
  label: menuItem.intl ? this.intl.t(menuItem.intl) : (menuItem.title ?? menuItem.label),
286
294
  description: menuItem.description,
287
295
  icon: menuItem.icon,
@@ -289,15 +297,83 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
289
297
  permission: menuItem.permission,
290
298
  visible: menuItem.visible,
291
299
  keywords: [menuItem.slug, menuItem.view, menuItem.section, menuItem.title, menuItem.label, menuItem.intl, ...(menuItem.keywords ?? [])].filter(Boolean),
292
- onClick: () => this.universe.transitionMenuItem(`${this.routePrefix}virtual`, menuItem),
300
+ activeWhen: () => isMenuItemActive(menuItem.section, menuItem.slug, menuItem.view),
293
301
  };
302
+
303
+ registryMenuItem.onClick = () => this.universe.transitionMenuItem(`${this.routePrefix}virtual`, registryMenuItem);
304
+
305
+ return registryMenuItem;
294
306
  }
295
307
 
296
308
  withRegistryItems(section, items) {
297
309
  const registryProperty = SECTION_REGISTRY_KEYS[section];
298
310
  const registryItems = (this[registryProperty] ?? []).filter((item) => !item.renderComponentInPlace).map((item) => this.registryItem(item));
299
311
 
300
- return [...items, ...registryItems];
312
+ return this.sortByPriority([...items, ...registryItems]);
313
+ }
314
+
315
+ sortByPriority(items = []) {
316
+ return [...items]
317
+ .map((item, index) => ({ item, index }))
318
+ .sort((a, b) => {
319
+ if (a.item.pinnedFirst !== b.item.pinnedFirst) {
320
+ return a.item.pinnedFirst ? -1 : 1;
321
+ }
322
+
323
+ const priorityOrder = (a.item.priority ?? 0) - (b.item.priority ?? 0);
324
+
325
+ if (priorityOrder !== 0) {
326
+ return priorityOrder;
327
+ }
328
+
329
+ return a.index - b.index;
330
+ })
331
+ .map(({ item }) => item);
332
+ }
333
+
334
+ defaultPriorityForRoute(route) {
335
+ const priorities = {
336
+ 'operations.orders': 0,
337
+ 'operations.orchestrator': 1,
338
+ 'operations.scheduler': 2,
339
+ 'operations.order-config': 3,
340
+ 'operations.service-rates': 4,
341
+ 'management.index': 0,
342
+ 'management.drivers': 1,
343
+ 'management.vehicles': 2,
344
+ 'management.fleets': 3,
345
+ 'management.vendors': 4,
346
+ 'management.contacts': 5,
347
+ 'management.places': 6,
348
+ 'management.fuel-reports': 7,
349
+ 'management.fuel-transactions': 8,
350
+ 'management.issues': 9,
351
+ 'maintenance.index': 0,
352
+ 'maintenance.schedules': 1,
353
+ 'maintenance.work-orders': 2,
354
+ 'maintenance.maintenances': 3,
355
+ 'maintenance.equipment': 4,
356
+ 'maintenance.parts': 5,
357
+ 'connectivity.telematics': 0,
358
+ 'connectivity.fuel-providers': 1,
359
+ 'connectivity.devices': 2,
360
+ 'connectivity.sensors': 3,
361
+ 'connectivity.events': 4,
362
+ 'analytics.index': 0,
363
+ 'analytics.reports': 1,
364
+ 'settings.index': 0,
365
+ 'settings.navigator-app': 1,
366
+ 'settings.map': 2,
367
+ 'settings.payments': 3,
368
+ 'settings.notifications': 4,
369
+ 'settings.routing': 5,
370
+ 'settings.orchestrator': 6,
371
+ 'settings.scheduling': 7,
372
+ 'settings.custom-fields': 8,
373
+ 'settings.avatars': 9,
374
+ };
375
+
376
+ return priorities[route] ?? 0;
301
377
  }
302
378
 
303
379
  fullRoute(route) {
@@ -314,6 +390,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
314
390
  }
315
391
  }
316
392
 
393
+ @action shouldSyncInitialActiveParent({ activePath = [], routeName }) {
394
+ 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);
397
+
398
+ return !isDefaultOrdersLanding;
399
+ }
400
+
317
401
  @action
318
402
  async searchNavigation({ query, limit = 12 }) {
319
403
  const trimmedQuery = query?.trim();
@@ -5,6 +5,7 @@ import { action } from '@ember/object';
5
5
  import { task } from 'ember-concurrency';
6
6
  import { isArray } from '@ember/array';
7
7
  import { startOfWeek, endOfWeek, format } from 'date-fns';
8
+ import calculateMapDrawerDropdownPosition from '../../../utils/map-drawer-dropdown-position';
8
9
 
9
10
  export default class MapDrawerDeviceEventListingComponent extends Component {
10
11
  @service store;
@@ -115,6 +116,7 @@ export default class MapDrawerDeviceEventListingComponent extends Component {
115
116
  ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.device-event') }),
116
117
  cellClassNames: 'overflow-visible',
117
118
  wrapperClass: 'flex items-center justify-end mx-2',
119
+ calculatePosition: calculateMapDrawerDropdownPosition,
118
120
  width: '10%',
119
121
  actions: [
120
122
  {
@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { action } from '@ember/object';
5
+ import calculateMapDrawerDropdownPosition from '../../../utils/map-drawer-dropdown-position';
5
6
 
6
7
  export default class MapDrawerDriverListingComponent extends Component {
7
8
  @service driverActions;
@@ -71,6 +72,7 @@ export default class MapDrawerDriverListingComponent extends Component {
71
72
  ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.driver') }),
72
73
  cellClassNames: 'overflow-visible',
73
74
  wrapperClass: 'flex items-center justify-end mx-2',
75
+ calculatePosition: calculateMapDrawerDropdownPosition,
74
76
  width: '90px',
75
77
  actions: [
76
78
  {
@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { action } from '@ember/object';
5
+ import calculateMapDrawerDropdownPosition from '../../../utils/map-drawer-dropdown-position';
5
6
 
6
7
  export default class MapDrawerPlaceListingComponent extends Component {
7
8
  @service placeActions;
@@ -53,6 +54,7 @@ export default class MapDrawerPlaceListingComponent extends Component {
53
54
  ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.place') }),
54
55
  cellClassNames: 'overflow-visible',
55
56
  wrapperClass: 'flex items-center justify-end mx-2',
57
+ calculatePosition: calculateMapDrawerDropdownPosition,
56
58
  width: '90px',
57
59
  actions: [
58
60
  {
@@ -4,23 +4,28 @@
4
4
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
5
5
  <PowerSelect
6
6
  @options={{this.trackables}}
7
- @selected={{this.resource}}
7
+ @selected={{this.selectedTrackable}}
8
8
  @onChange={{this.onResourceSelected}}
9
+ @searchEnabled={{true}}
10
+ @searchField="trackableSearchText"
9
11
  @placeholder={{t "common.select-field" field="trackable"}}
10
12
  @triggerClass="form-select form-input form-input-sm w-48"
11
13
  as |option|
12
14
  >
13
15
  <div class="flex flex-row items-start space-x-2">
14
16
  <div class="inline-block">
15
- {{#if (eq (get-model-name option) "driver")}}
16
- <Image src={{option.photo_url}} @fallbackSrc={{config "defaultValues.driverAvatar"}} class="w-4 h-4" />
17
+ {{#if (eq option.modelName "driver")}}
18
+ <Image src={{option.resource.photo_url}} @fallbackSrc={{config "defaultValues.driverAvatar"}} class="w-4 h-4" />
17
19
  {{else}}
18
- <Image src={{option.avatar_url}} @fallbackSrc={{config "defaultValues.vehicleAvatar"}} class="w-4 h-4" />
20
+ <Image src={{option.resource.avatar_url}} @fallbackSrc={{config "defaultValues.vehicleAvatar"}} class="w-4 h-4" />
19
21
  {{/if}}
20
22
  </div>
21
23
  <div class="inline-block text-sm truncate">
22
- <div class="font-semibold text-sm normalize-in-trigger">{{or option.name option.displayName option.public_id}}</div>
23
- <div class="hide-from-trigger text-xs">{{or option.email option.serial_number option.plate_number option.internal_id}}</div>
24
+ <div class="font-semibold text-sm normalize-in-trigger">{{option.primaryLabel}}</div>
25
+ <div class="hide-from-trigger text-xs">{{option.secondaryLabel}}</div>
26
+ {{#if option.deviceLabel}}
27
+ <div class="hide-from-trigger text-xs text-gray-500 dark:text-gray-400 truncate">{{option.deviceLabel}}</div>
28
+ {{/if}}
24
29
  </div>
25
30
  </div>
26
31
  </PowerSelect>
@@ -8,6 +8,7 @@ import { htmlSafe } from '@ember/template';
8
8
  import { startOfWeek, endOfWeek, format } from 'date-fns';
9
9
  import getModelName from '@fleetbase/ember-core/utils/get-model-name';
10
10
  import ensureLeafletDrawEditNamespace from '../../../utils/leaflet-draw-namespace-guard';
11
+ import { buildTrackableOption } from '../../../utils/trackable-option';
11
12
 
12
13
  const L = window.leaflet || window.L;
13
14
 
@@ -29,6 +30,7 @@ export default class MapDrawerPositionListingComponent extends Component {
29
30
  @tracked replaySpeed = '1';
30
31
  @tracked positionsLayer = null;
31
32
  @tracked positionOverlayIds = [];
33
+ @tracked selectedTrackableModelName = null;
32
34
 
33
35
  /** Computed properties - read state from service */
34
36
  get isReplaying() {
@@ -47,7 +49,17 @@ export default class MapDrawerPositionListingComponent extends Component {
47
49
  const vehicles = this.mapManager.livemap?.vehicles ?? [];
48
50
  const drivers = this.mapManager.livemap?.drivers ?? [];
49
51
 
50
- return [...vehicles, ...drivers];
52
+ return [...vehicles.map((vehicle) => buildTrackableOption(vehicle, 'vehicle')), ...drivers.map((driver) => buildTrackableOption(driver, 'driver'))];
53
+ }
54
+
55
+ get selectedTrackable() {
56
+ if (!this.resource) {
57
+ return null;
58
+ }
59
+
60
+ const resourceType = this.resourceType;
61
+
62
+ return this.trackables.find((trackable) => trackable.resource?.id === this.resource?.id && trackable.modelName === resourceType) ?? null;
51
63
  }
52
64
 
53
65
  get replayProgressWidth() {
@@ -72,7 +84,7 @@ export default class MapDrawerPositionListingComponent extends Component {
72
84
  if (!this.resource) {
73
85
  return 'resource';
74
86
  }
75
- return getModelName(this.resource) || 'resource';
87
+ return this.selectedTrackableModelName || getModelName(this.resource) || 'resource';
76
88
  }
77
89
 
78
90
  get hasPositions() {
@@ -179,8 +191,9 @@ export default class MapDrawerPositionListingComponent extends Component {
179
191
  this.positionOverlayIds = [];
180
192
  }
181
193
 
182
- @action onResourceSelected(resource) {
183
- this.resource = resource;
194
+ @action onResourceSelected(trackable) {
195
+ this.resource = trackable?.resource ?? null;
196
+ this.selectedTrackableModelName = trackable?.modelName ?? null;
184
197
  this.loadPositions.perform();
185
198
  }
186
199
 
@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { action } from '@ember/object';
5
+ import calculateMapDrawerDropdownPosition from '../../../utils/map-drawer-dropdown-position';
5
6
 
6
7
  export default class MapDrawerVehicleListingComponent extends Component {
7
8
  @service vehicleActions;
@@ -65,6 +66,7 @@ export default class MapDrawerVehicleListingComponent extends Component {
65
66
  ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.vehicle') }),
66
67
  cellClassNames: 'overflow-visible',
67
68
  wrapperClass: 'flex items-center justify-end mx-2',
69
+ calculatePosition: calculateMapDrawerDropdownPosition,
68
70
  width: '90px',
69
71
  actions: [
70
72
  {