@fleetbase/fleetops-engine 0.6.54 → 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 (127) 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/form.hbs +2 -2
  19. package/addon/components/device/form.js +7 -0
  20. package/addon/components/device/panel-header.hbs +45 -20
  21. package/addon/components/device/panel-header.js +49 -1
  22. package/addon/components/device/panel-tabs/events.hbs +30 -0
  23. package/addon/components/device/panel-tabs/events.js +128 -0
  24. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  25. package/addon/components/device/panel-tabs/sensors.js +93 -0
  26. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  27. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  28. package/addon/components/equipment/form.js +8 -1
  29. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  30. package/addon/components/layout/fleet-ops-sidebar.js +8 -0
  31. package/addon/components/map/drawer/device-event-listing.js +2 -0
  32. package/addon/components/map/drawer/driver-listing.js +2 -0
  33. package/addon/components/map/drawer/place-listing.js +2 -0
  34. package/addon/components/map/drawer/position-listing.hbs +11 -6
  35. package/addon/components/map/drawer/position-listing.js +17 -4
  36. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  37. package/addon/components/sensor/panel-header.hbs +47 -15
  38. package/addon/components/sensor/panel-header.js +59 -1
  39. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  40. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  41. package/addon/components/table/cell/dropdown.hbs +28 -0
  42. package/addon/components/table/cell/dropdown.js +91 -0
  43. package/addon/components/vendor/panel-header.hbs +82 -0
  44. package/addon/components/vendor/panel-header.js +48 -0
  45. package/addon/controllers/connectivity/devices/index/details/vehicle.js +49 -1
  46. package/addon/controllers/connectivity/devices/index.js +144 -129
  47. package/addon/controllers/connectivity/events/details.js +3 -0
  48. package/addon/controllers/connectivity/events/index.js +187 -11
  49. package/addon/controllers/connectivity/sensors/index.js +156 -14
  50. package/addon/controllers/connectivity/telematics/details/devices.js +126 -134
  51. package/addon/controllers/maintenance/equipment/index.js +1 -2
  52. package/addon/controllers/maintenance/parts/index.js +1 -2
  53. package/addon/controllers/management/drivers/index.js +82 -42
  54. package/addon/controllers/management/fuel-reports/index.js +63 -4
  55. package/addon/controllers/management/issues/index.js +63 -4
  56. package/addon/controllers/management/vehicles/index.js +33 -6
  57. package/addon/routes/connectivity/devices/index/details/vehicle.js +24 -2
  58. package/addon/routes/connectivity/devices/index.js +7 -0
  59. package/addon/routes/connectivity/events/details.js +3 -0
  60. package/addon/routes/connectivity/events/index.js +6 -2
  61. package/addon/routes/connectivity/sensors/index.js +7 -2
  62. package/addon/routes/connectivity/telematics/details/devices.js +2 -0
  63. package/addon/services/device-actions.js +30 -0
  64. package/addon/services/device-event-actions.js +1 -1
  65. package/addon/services/equipment-actions.js +1 -0
  66. package/addon/services/sensor-actions.js +3 -0
  67. package/addon/services/service-rate-actions.js +39 -1
  68. package/addon/services/vehicle-actions.js +24 -12
  69. package/addon/services/vendor-actions.js +3 -0
  70. package/addon/styles/fleetops-engine.css +8 -0
  71. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +64 -1
  72. package/addon/templates/management/vendors/index/details.hbs +2 -2
  73. package/addon/templates/settings/notifications.hbs +2 -1
  74. package/addon/templates/settings/routing.hbs +1 -0
  75. package/addon/utils/device-table-columns.js +288 -0
  76. package/addon/utils/identity-cell-resource.js +20 -0
  77. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  78. package/addon/utils/trackable-option.js +93 -0
  79. package/app/components/cell/attached-vehicle.js +1 -0
  80. package/app/components/cell/device-identity.js +1 -0
  81. package/app/components/cell/driver-identity.js +1 -0
  82. package/app/components/cell/equipment-identity.js +1 -0
  83. package/app/components/cell/part-identity.js +1 -0
  84. package/app/components/cell/telematic-device.js +1 -0
  85. package/app/components/cell/vehicle-identity.js +1 -0
  86. package/app/components/device/panel-tabs/events.hbs +30 -0
  87. package/app/components/device/panel-tabs/events.js +128 -0
  88. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  89. package/app/components/device/panel-tabs/sensors.js +93 -0
  90. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  91. package/app/components/device/panel-tabs/vehicle.js +90 -0
  92. package/app/components/table/cell/dropdown/action-item.js +1 -0
  93. package/app/components/table/cell/dropdown.js +1 -0
  94. package/app/components/vendor/panel-header.js +1 -0
  95. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  96. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  97. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  98. package/app/utils/map-drawer-dropdown-position.js +1 -0
  99. package/app/utils/trackable-option.js +1 -0
  100. package/composer.json +1 -1
  101. package/extension.json +1 -1
  102. package/package.json +4 -4
  103. package/server/src/Console/Commands/SyncTelematics.php +4 -4
  104. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -1
  105. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +1 -1
  106. package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
  107. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  108. package/server/src/Http/Filter/DeviceEventFilter.php +21 -0
  109. package/server/src/Http/Filter/DeviceFilter.php +24 -1
  110. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  111. package/server/src/Models/Device.php +2 -0
  112. package/server/src/Models/DeviceEvent.php +81 -0
  113. package/server/src/Models/Equipment.php +55 -0
  114. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +1 -0
  115. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  116. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  117. package/server/src/Support/Telematics/Providers/SafeeProvider.php +26 -2
  118. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  119. package/server/src/routes.php +0 -1
  120. package/server/tests/DeviceFilterTest.php +9 -0
  121. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  122. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  123. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  124. package/server/tests/TelematicsHardeningTest.php +238 -7
  125. package/addon/controllers/connectivity/events/index/details.js +0 -3
  126. package/addon/routes/connectivity/events/index/details.js +0 -3
  127. /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
@@ -66,6 +66,26 @@ export default class VehicleActionsService extends ResourceActionService {
66
66
  this.initialize('vehicle', { status: 'available' });
67
67
  }
68
68
 
69
+ async resolveVehicleResource(vehicle) {
70
+ if (typeof vehicle?.then === 'function') {
71
+ return await vehicle;
72
+ }
73
+
74
+ if (typeof vehicle?.loadResource === 'function') {
75
+ return (await vehicle.loadResource()) ?? vehicle;
76
+ }
77
+
78
+ return vehicle;
79
+ }
80
+
81
+ async reloadIndexResource(vehicle) {
82
+ if (vehicle?.meta?._index_resource) {
83
+ await vehicle.reload();
84
+ }
85
+
86
+ return vehicle;
87
+ }
88
+
69
89
  transition = {
70
90
  view: (vehicle) => this.transitionTo('management.vehicles.index.details', vehicle),
71
91
  edit: (vehicle) => this.transitionTo('management.vehicles.index.edit', vehicle),
@@ -87,9 +107,7 @@ export default class VehicleActionsService extends ResourceActionService {
87
107
  });
88
108
  },
89
109
  edit: async (vehicle, options = {}) => {
90
- if (vehicle?.meta?._index_resource) {
91
- await vehicle.reload();
92
- }
110
+ vehicle = await this.reloadIndexResource(await this.resolveVehicleResource(vehicle));
93
111
 
94
112
  return this.resourceContextPanel.open({
95
113
  content: 'vehicle/form',
@@ -109,9 +127,7 @@ export default class VehicleActionsService extends ResourceActionService {
109
127
  });
110
128
  },
111
129
  view: async (vehicle, options = {}) => {
112
- if (vehicle?.meta?._index_resource) {
113
- await vehicle.reload();
114
- }
130
+ vehicle = await this.reloadIndexResource(await this.resolveVehicleResource(vehicle));
115
131
 
116
132
  return this.resourceContextPanel.open({
117
133
  vehicle,
@@ -144,9 +160,7 @@ export default class VehicleActionsService extends ResourceActionService {
144
160
  });
145
161
  },
146
162
  edit: async (vehicle, options = {}, saveOptions = {}) => {
147
- if (vehicle?.meta?._index_resource) {
148
- await vehicle.reload();
149
- }
163
+ vehicle = await this.reloadIndexResource(await this.resolveVehicleResource(vehicle));
150
164
 
151
165
  return this.modalsManager.show('modals/resource', {
152
166
  resource: vehicle,
@@ -159,9 +173,7 @@ export default class VehicleActionsService extends ResourceActionService {
159
173
  });
160
174
  },
161
175
  view: async (vehicle, options = {}) => {
162
- if (vehicle?.meta?._index_resource) {
163
- await vehicle.reload();
164
- }
176
+ vehicle = await this.reloadIndexResource(await this.resolveVehicleResource(vehicle));
165
177
 
166
178
  return this.modalsManager.show('modals/resource', {
167
179
  resource: vehicle,
@@ -43,8 +43,11 @@ export default class VendorActionsService extends ResourceActionService {
43
43
  view: (vendor) => {
44
44
  return this.resourceContextPanel.open({
45
45
  vendor,
46
+ header: 'vendor/panel-header',
46
47
  tabs: [
47
48
  {
49
+ key: 'overview',
50
+ id: 'overview',
48
51
  label: this.intl.t('common.overview'),
49
52
  component: 'vendor/details',
50
53
  },
@@ -294,6 +294,14 @@ body[data-theme='dark'] .fleetops-connectivity-kpi-accent-rose .fleetops-connect
294
294
  color: #fde047;
295
295
  }
296
296
 
297
+ .fleetops-device-status-badge.status-badge > span {
298
+ padding: 0 0.25rem !important;
299
+ border-radius: 0.125rem !important;
300
+ font-size: 0.5625rem !important;
301
+ line-height: 0.75rem !important;
302
+ font-weight: 700;
303
+ }
304
+
297
305
  .fleetops-provider-connections-panel .table-wrapper table tbody tr td,
298
306
  .fleetops-provider-connections-panel .next-table-wrapper table tbody tr td {
299
307
  vertical-align: top;
@@ -7,9 +7,12 @@
7
7
 
8
8
  {{#if this.hasVehicle}}
9
9
  <div class="flex flex-nowrap items-center gap-2 whitespace-nowrap">
10
- {{#if this.vehicle.id}}
10
+ {{#if this.canOpenVehicle}}
11
11
  <Button @icon="eye" @text="Open Vehicle" @size="xs" @onClick={{this.openVehicle}} />
12
12
  {{/if}}
13
+ {{#if this.canLocateVehicle}}
14
+ <Button @icon="location-dot" @text="Locate" @size="xs" @onClick={{this.locateVehicle}} />
15
+ {{/if}}
13
16
  <Button @icon="shuffle" @text="Change Vehicle" @size="xs" @onClick={{this.attachToVehicle}} />
14
17
  <Button @icon="unlink" @text="Detach" @size="xs" @type="danger" @onClick={{this.detachFromVehicle}} />
15
18
  </div>
@@ -70,6 +73,66 @@
70
73
  </div>
71
74
  </div>
72
75
  </section>
76
+
77
+ <section class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
78
+ <div class="flex items-start justify-between gap-3 border-b border-gray-200 px-3 py-2 dark:border-gray-700">
79
+ <div class="min-w-0">
80
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Latest Positions</h3>
81
+ <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">Recent vehicle coordinates reported through this device attachment.</p>
82
+ </div>
83
+ {{#if this.canOpenVehicle}}
84
+ <Button @icon="route" @text="Full History" @size="xs" @onClick={{this.openVehiclePositions}} />
85
+ {{/if}}
86
+ </div>
87
+
88
+ {{#if this.hasPositions}}
89
+ <div class="grid grid-cols-1 gap-0 divide-y divide-gray-100 dark:divide-gray-700">
90
+ {{#each this.positions as |position index|}}
91
+ <div class="flex flex-wrap items-center justify-between gap-3 px-3 py-2 text-xs">
92
+ <div class="flex min-w-0 items-center gap-2">
93
+ <div class="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-300">
94
+ <span class="font-bold">{{add index 1}}</span>
95
+ </div>
96
+ <div class="min-w-0">
97
+ <div class="truncate font-semibold text-gray-900 dark:text-gray-100">{{n-a (format-date-fns position.created_at "dd MMM yyyy, HH:mm")}}</div>
98
+ <div class="truncate text-gray-500 dark:text-gray-400">{{n-a (point-coordinates position.coordinates)}}</div>
99
+ </div>
100
+ </div>
101
+ <div class="grid grid-cols-2 gap-2 text-right text-gray-500 dark:text-gray-400 sm:grid-cols-3">
102
+ <div>
103
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Speed</div>
104
+ <div class="text-gray-800 dark:text-gray-100">{{n-a position.speed}}</div>
105
+ </div>
106
+ <div>
107
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Heading</div>
108
+ <div class="text-gray-800 dark:text-gray-100">{{n-a position.heading position.bearing}}</div>
109
+ </div>
110
+ <div>
111
+ <div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Altitude</div>
112
+ <div class="text-gray-800 dark:text-gray-100">{{n-a position.altitude}}</div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ {{/each}}
117
+ </div>
118
+
119
+ <div class="border-t border-gray-200 dark:border-gray-700">
120
+ <PositionsReplay @resource={{this.vehicle}} @mapHeight={{180}} class="max-h-[360px] overflow-hidden" />
121
+ </div>
122
+ {{else}}
123
+ <div class="p-3">
124
+ <Telematic::TabEmptyState
125
+ @tone="info"
126
+ @icon="location-dot"
127
+ @title="No recent positions"
128
+ @message="This attached vehicle does not have recent position history available yet."
129
+ @primaryText="Locate Vehicle"
130
+ @primaryIcon="location-dot"
131
+ @primaryAction={{this.locateVehicle}}
132
+ />
133
+ </div>
134
+ {{/if}}
135
+ </section>
73
136
  {{else}}
74
137
  <Telematic::TabEmptyState
75
138
  @tone="warning"
@@ -1,7 +1,7 @@
1
1
  <Layout::Resource::Panel
2
2
  @resource={{@model}}
3
3
  @controller={{this}}
4
- @headerTitle={{or @model.name}}
4
+ @headerComponent={{component "vendor/panel-header"}}
5
5
  @actionButtons={{this.actionButtons}}
6
6
  @onPressCancel={{transition-to "management.vendors.index"}}
7
7
  @onOverlayReady={{fn (mut this.overlay)}}
@@ -11,4 +11,4 @@
11
11
  <TabNavigation @tabs={{this.tabs}} @contentClass="scrollable" @tablistClass="pl-2">
12
12
  {{outlet}}
13
13
  </TabNavigation>
14
- </Layout::Resource::Panel>
14
+ </Layout::Resource::Panel>
@@ -19,6 +19,7 @@
19
19
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
20
20
  <PowerSelectMultiple
21
21
  @searchEnabled={{true}}
22
+ @searchField="label"
22
23
  @options={{this.notifiables}}
23
24
  @selected={{get this.notificationSettings (concat (get-notification-key notification.definition notification.name) ".notifiables")}}
24
25
  @onChange={{fn this.onSelectNotifiable notification}}
@@ -35,4 +36,4 @@
35
36
  </div>
36
37
  </div>
37
38
  <Spacer @height="600px" />
38
- </Layout::Section::Body>
39
+ </Layout::Section::Body>
@@ -64,6 +64,7 @@
64
64
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
65
65
  <PowerSelectMultiple
66
66
  @searchEnabled={{true}}
67
+ @searchField="label"
67
68
  @options={{this.trackingProviderOptions}}
68
69
  @selected={{this.selectedTrackingFallbackOptions}}
69
70
  @onChange={{this.setTrackingFallbacks}}
@@ -0,0 +1,288 @@
1
+ const connectionStatusOptions = [
2
+ { label: 'Online', value: 'online' },
3
+ { label: 'Recently Offline', value: 'recently_offline' },
4
+ { label: 'Offline', value: 'offline' },
5
+ { label: 'Long Offline', value: 'long_offline' },
6
+ { label: 'Never Connected', value: 'never_connected' },
7
+ ];
8
+
9
+ const attachmentStateOptions = [
10
+ { label: 'Attached', value: 'attached' },
11
+ { label: 'Unattached', value: 'unattached' },
12
+ ];
13
+
14
+ export { connectionStatusOptions, attachmentStateOptions };
15
+
16
+ export default function buildDeviceTableColumns(controller, options = {}) {
17
+ const { showProvider = true, showActions = true, deviceActionMode = 'route', showDeviceStatus = true } = options;
18
+ const viewDevice =
19
+ deviceActionMode === 'panel'
20
+ ? (controller.deviceActions.panel?.view ?? controller.deviceActions.transition?.view)
21
+ : (controller.deviceActions.transition?.view ?? controller.deviceActions.panel?.view);
22
+ const editDevice =
23
+ deviceActionMode === 'panel'
24
+ ? (controller.deviceActions.panel?.edit ?? controller.deviceActions.transition?.edit)
25
+ : (controller.deviceActions.transition?.edit ?? controller.deviceActions.panel?.edit);
26
+
27
+ const columns = [
28
+ {
29
+ sticky: true,
30
+ label: 'Telematic Device',
31
+ valuePath: 'displayName',
32
+ cellComponent: 'cell/device-identity',
33
+ action: viewDevice,
34
+ compact: true,
35
+ permission: 'fleet-ops view device',
36
+ resizable: true,
37
+ sortable: true,
38
+ filterable: true,
39
+ filterParam: 'query',
40
+ filterComponent: 'filter/string',
41
+ showStatus: showDeviceStatus,
42
+ },
43
+ {
44
+ label: 'Connection',
45
+ valuePath: 'connection_status',
46
+ cellComponent: 'table/cell/status',
47
+ resizable: true,
48
+ sortable: false,
49
+ filterable: true,
50
+ filterParam: 'connection_status',
51
+ filterComponent: 'filter/multi-option',
52
+ filterOptions: connectionStatusOptions,
53
+ filterOptionLabel: 'label',
54
+ filterOptionValue: 'value',
55
+ },
56
+ ];
57
+ const providerIdColumn = {
58
+ label: 'Provider ID / IMEI',
59
+ valuePath: 'device_id',
60
+ resizable: true,
61
+ sortable: true,
62
+ filterable: true,
63
+ filterParam: 'device_id',
64
+ filterComponent: 'filter/string',
65
+ };
66
+ const vehicleColumn = {
67
+ label: 'Vehicle',
68
+ valuePath: 'attached_to_name',
69
+ cellComponent: 'cell/vehicle-identity',
70
+ action: async (vehicle) => {
71
+ const resolvedVehicle = vehicle?.loadResource ? ((await vehicle.loadResource()) ?? vehicle) : vehicle;
72
+
73
+ if (resolvedVehicle?.id && controller.vehicleActions?.panel?.view) {
74
+ return controller.vehicleActions.panel.view(resolvedVehicle);
75
+ }
76
+ },
77
+ compact: true,
78
+ permission: 'fleet-ops view vehicle',
79
+ showStatusBadge: true,
80
+ emptyText: '-',
81
+ resourcePath: (device) => {
82
+ const attachableType = `${device?.attachable_type ?? ''}`.toLowerCase();
83
+
84
+ if (!device?.attachable_uuid || (attachableType && !attachableType.includes('vehicle'))) {
85
+ return null;
86
+ }
87
+
88
+ return (
89
+ device.attachable ?? {
90
+ id: device.attachable_uuid,
91
+ displayName: device.attached_to_name,
92
+ display_name: device.attached_to_name,
93
+ name: device.attached_to_name,
94
+ public_id: device.attachable_uuid,
95
+ vehicle_number: device.plate_number ?? device.call_sign ?? device.vehicle_number ?? device.attachable_uuid,
96
+ loadResource: () => controller.resolveAttachedVehicle?.(device),
97
+ }
98
+ );
99
+ },
100
+ resizable: true,
101
+ sortable: false,
102
+ filterable: true,
103
+ filterParam: 'vehicle',
104
+ filterComponent: 'filter/model',
105
+ filterComponentPlaceholder: 'Select vehicle',
106
+ model: 'vehicle',
107
+ modelNamePath: 'displayName',
108
+ };
109
+
110
+ if (showProvider) {
111
+ columns.push({
112
+ label: 'Telematic Provider',
113
+ valuePath: 'telematic_name',
114
+ cellComponent: 'cell/telematic-provider',
115
+ compact: true,
116
+ action: controller.openTelematic,
117
+ permission: 'fleet-ops view telematic',
118
+ resizable: true,
119
+ sortable: false,
120
+ filterable: true,
121
+ filterParam: 'telematic',
122
+ filterComponent: 'filter/model',
123
+ filterComponentPlaceholder: 'Select telematic',
124
+ model: 'telematic',
125
+ });
126
+ columns.push(vehicleColumn, providerIdColumn);
127
+ } else {
128
+ columns.push(providerIdColumn, vehicleColumn);
129
+ }
130
+
131
+ columns.push(
132
+ {
133
+ label: 'Sensors',
134
+ valuePath: 'sensors_count',
135
+ resizable: true,
136
+ sortable: false,
137
+ filterable: false,
138
+ },
139
+ {
140
+ label: 'Last Seen',
141
+ valuePath: 'lastOnlineAt',
142
+ sortParam: 'last_online_at',
143
+ resizable: true,
144
+ sortable: true,
145
+ filterable: true,
146
+ filterParam: 'last_online_at',
147
+ filterComponent: 'filter/date',
148
+ },
149
+ {
150
+ label: 'Provider',
151
+ valuePath: 'provider',
152
+ hidden: true,
153
+ resizable: true,
154
+ sortable: true,
155
+ filterable: true,
156
+ filterParam: 'provider',
157
+ filterComponent: 'filter/string',
158
+ },
159
+ {
160
+ label: 'Type',
161
+ valuePath: 'type',
162
+ hidden: true,
163
+ resizable: true,
164
+ sortable: true,
165
+ filterable: true,
166
+ filterParam: 'type',
167
+ filterComponent: 'filter/multi-option',
168
+ filterOptions: controller.deviceTypeOptions,
169
+ },
170
+ {
171
+ label: 'Serial Number',
172
+ valuePath: 'serial_number',
173
+ hidden: true,
174
+ resizable: true,
175
+ sortable: true,
176
+ filterable: true,
177
+ filterParam: 'serial_number',
178
+ filterComponent: 'filter/string',
179
+ },
180
+ {
181
+ label: controller.intl.t('column.status'),
182
+ valuePath: 'status',
183
+ cellComponent: 'table/cell/status',
184
+ hidden: true,
185
+ resizable: true,
186
+ sortable: true,
187
+ filterable: true,
188
+ filterParam: 'status',
189
+ filterComponent: 'filter/multi-option',
190
+ filterOptions: controller.deviceStatusOptions,
191
+ },
192
+ {
193
+ label: 'Attachment',
194
+ valuePath: 'attachable_uuid',
195
+ hidden: true,
196
+ resizable: true,
197
+ sortable: false,
198
+ filterable: true,
199
+ filterParam: 'attachment_state',
200
+ filterComponent: 'filter/select',
201
+ filterOptions: attachmentStateOptions,
202
+ filterOptionLabel: 'label',
203
+ filterOptionValue: 'value',
204
+ },
205
+ {
206
+ label: controller.intl.t('column.updated-at'),
207
+ valuePath: 'updatedAt',
208
+ sortParam: 'updated_at',
209
+ resizable: true,
210
+ sortable: true,
211
+ hidden: true,
212
+ filterable: true,
213
+ filterComponent: 'filter/date',
214
+ }
215
+ );
216
+
217
+ if (showActions) {
218
+ columns.push({
219
+ label: '',
220
+ cellComponent: 'table/cell/dropdown',
221
+ ddButtonText: false,
222
+ ddButtonIcon: 'ellipsis-h',
223
+ ddButtonIconPrefix: 'fas',
224
+ ddMenuLabel: controller.intl.t('common.resource-actions', { resource: controller.intl.t('resource.device') }),
225
+ cellClassNames: 'overflow-visible align-middle',
226
+ wrapperClass: 'flex items-center justify-end mx-2',
227
+ sticky: 'right',
228
+ width: 60,
229
+ actions: [
230
+ {
231
+ label: controller.intl.t('common.view-resource', { resource: controller.intl.t('resource.device') }),
232
+ fn: viewDevice,
233
+ permission: 'fleet-ops view device',
234
+ },
235
+ {
236
+ label: controller.intl.t('common.edit-resource', { resource: controller.intl.t('resource.device') }),
237
+ fn: editDevice,
238
+ permission: 'fleet-ops update device',
239
+ },
240
+ {
241
+ label: 'Attach or change vehicle',
242
+ fn: controller.openAttachDeviceModal ?? controller.deviceActions.attachToVehicle,
243
+ permission: 'fleet-ops update device',
244
+ },
245
+ {
246
+ label: controller.intl.t('device.actions.detach-from-vehicle'),
247
+ fn: controller.deviceActions.detachFromVehicle,
248
+ permission: 'fleet-ops update device',
249
+ isVisible: controller.hasAttachedVehicle,
250
+ },
251
+ {
252
+ separator: true,
253
+ isVisible: controller.hasAttachedVehicle,
254
+ },
255
+ {
256
+ label: 'View attached vehicle',
257
+ fn: controller.viewAttachedVehicle,
258
+ permission: 'fleet-ops view vehicle',
259
+ isVisible: controller.hasAttachedVehicle,
260
+ },
261
+ {
262
+ label: 'Locate attached vehicle on map',
263
+ fn: controller.locateAttachedVehicle,
264
+ permission: 'fleet-ops view vehicle',
265
+ isVisible: controller.hasAttachedVehicle,
266
+ },
267
+ ...(controller.openDeviceEvents
268
+ ? [
269
+ {
270
+ separator: true,
271
+ },
272
+ {
273
+ label: 'Review recent events',
274
+ fn: controller.openDeviceEvents,
275
+ permission: 'fleet-ops view device-event',
276
+ },
277
+ ]
278
+ : []),
279
+ ],
280
+ sortable: false,
281
+ filterable: false,
282
+ resizable: false,
283
+ searchable: false,
284
+ });
285
+ }
286
+
287
+ return columns;
288
+ }
@@ -0,0 +1,20 @@
1
+ import { get } from '@ember/object';
2
+
3
+ export function resolveIdentityCellResource(args) {
4
+ const column = args.column ?? {};
5
+ const resourcePath = column.resourcePath;
6
+
7
+ if (typeof resourcePath === 'function') {
8
+ return resourcePath(args.row, args.value, column) ?? null;
9
+ }
10
+
11
+ if (typeof resourcePath === 'string') {
12
+ return get(args.row, resourcePath) ?? null;
13
+ }
14
+
15
+ if (args.value && typeof args.value === 'object') {
16
+ return args.value;
17
+ }
18
+
19
+ return args.row;
20
+ }
@@ -0,0 +1,47 @@
1
+ export default function calculateMapDrawerDropdownPosition(trigger, content) {
2
+ const dropdownRoot = trigger?.closest?.('.ember-basic-dropdown');
3
+ const drawerPanel = trigger?.closest?.('.next-drawer-panel');
4
+
5
+ if (!trigger || !dropdownRoot) {
6
+ return { style: {} };
7
+ }
8
+
9
+ const triggerRect = trigger.getBoundingClientRect();
10
+ const rootRect = dropdownRoot.getBoundingClientRect();
11
+ const drawerRect = drawerPanel?.getBoundingClientRect?.();
12
+ const contentRect = content?.getBoundingClientRect?.();
13
+ const contentWidth = contentRect?.width || 224;
14
+ // const contentHeight = contentRect?.height || 240;
15
+ const gap = 6;
16
+
17
+ const viewportRect = {
18
+ top: 0,
19
+ right: window.innerWidth,
20
+ bottom: window.innerHeight,
21
+ left: 0,
22
+ };
23
+ const boundaryRect = drawerRect ?? viewportRect;
24
+ const minLeft = boundaryRect.left - rootRect.left + gap;
25
+ const maxLeft = boundaryRect.right - rootRect.left - contentWidth - gap;
26
+ // const minTop = boundaryRect.top - rootRect.top + gap;
27
+ // const maxTop = boundaryRect.bottom - rootRect.top - contentHeight - gap;
28
+
29
+ const preferredLeft = triggerRect.left - rootRect.left - contentWidth - gap;
30
+ // const preferredTop = triggerRect.top - rootRect.top;
31
+ const left = clamp(preferredLeft, minLeft, Math.max(minLeft, maxLeft));
32
+ const top = 0; //clamp(preferredTop, minTop, Math.max(minTop, maxTop));
33
+
34
+ return {
35
+ style: {
36
+ position: 'absolute',
37
+ left: `${left}px`,
38
+ top: `${top}px`,
39
+ marginTop: '0px',
40
+ zIndex: '10000',
41
+ },
42
+ };
43
+ }
44
+
45
+ function clamp(value, min, max) {
46
+ return Math.min(Math.max(value, min), max);
47
+ }
@@ -0,0 +1,93 @@
1
+ import getModelName from '@fleetbase/ember-core/utils/get-model-name';
2
+ import { isArray } from '@ember/array';
3
+ import { get } from '@ember/object';
4
+
5
+ export function buildTrackableOption(resource, modelName = getTrackableModelName(resource)) {
6
+ const deviceLabel = trackableDeviceLabel(resource, modelName);
7
+ const secondaryLabel = trackableSecondaryLabel(resource, modelName);
8
+ const primaryLabel = getValue(resource, 'name') ?? getValue(resource, 'displayName') ?? getValue(resource, 'display_name') ?? getValue(resource, 'public_id');
9
+ const trackableSearchText = [
10
+ primaryLabel,
11
+ secondaryLabel,
12
+ getValue(resource, 'public_id'),
13
+ getValue(resource, 'internal_id'),
14
+ getValue(resource, 'serial_number'),
15
+ getValue(resource, 'plate_number'),
16
+ getValue(resource, 'email'),
17
+ getValue(resource, 'vin'),
18
+ getValue(resource, 'call_sign'),
19
+ deviceLabel,
20
+ ]
21
+ .filter(Boolean)
22
+ .join(' ');
23
+
24
+ return {
25
+ resource,
26
+ modelName,
27
+ primaryLabel,
28
+ secondaryLabel,
29
+ deviceLabel,
30
+ trackableSearchText,
31
+ };
32
+ }
33
+
34
+ export function trackableSecondaryLabel(resource, modelName = getTrackableModelName(resource)) {
35
+ if (modelName === 'driver') {
36
+ return getValue(resource, 'email') ?? getValue(resource, 'phone') ?? getValue(resource, 'vehicle_name') ?? getValue(resource, 'internal_id') ?? getValue(resource, 'public_id');
37
+ }
38
+
39
+ return getValue(resource, 'plate_number') ?? getValue(resource, 'serial_number') ?? getValue(resource, 'internal_id') ?? getValue(resource, 'vin') ?? getValue(resource, 'public_id');
40
+ }
41
+
42
+ export function trackableDeviceLabel(resource, modelName = getTrackableModelName(resource)) {
43
+ const devices = trackableDevices(resource, modelName);
44
+ const labels = devices.map(deviceLabel).filter(Boolean);
45
+
46
+ if (labels.length === 0) {
47
+ return null;
48
+ }
49
+
50
+ return labels.length === 1 ? `Device: ${labels[0]}` : `Devices: ${labels.join(', ')}`;
51
+ }
52
+
53
+ export function trackableDevices(resource, modelName = getTrackableModelName(resource)) {
54
+ const devices = modelName === 'driver' ? getValue(resource, 'vehicle.devices') : getValue(resource, 'devices');
55
+
56
+ if (!devices) {
57
+ return [];
58
+ }
59
+
60
+ if (isArray(devices)) {
61
+ return devices;
62
+ }
63
+
64
+ if (typeof devices.toArray === 'function') {
65
+ return devices.toArray();
66
+ }
67
+
68
+ return [];
69
+ }
70
+
71
+ export function deviceLabel(device) {
72
+ return (
73
+ getValue(device, 'displayName') ??
74
+ getValue(device, 'display_name') ??
75
+ getValue(device, 'name') ??
76
+ getValue(device, 'device_id') ??
77
+ getValue(device, 'serial_number') ??
78
+ getValue(device, 'imei') ??
79
+ getValue(device, 'public_id')
80
+ );
81
+ }
82
+
83
+ function getTrackableModelName(resource) {
84
+ return getModelName(resource) ?? getValue(resource, 'modelName') ?? resource?.constructor?.modelName;
85
+ }
86
+
87
+ function getValue(object, path) {
88
+ if (!object) {
89
+ return undefined;
90
+ }
91
+
92
+ return get(object, path);
93
+ }
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/cell/attached-vehicle';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/cell/device-identity';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/cell/driver-identity';