@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.
- package/addon/components/admin/routing-settings.hbs +2 -1
- package/addon/components/cell/attached-vehicle.hbs +10 -0
- package/addon/components/cell/attached-vehicle.js +58 -0
- package/addon/components/cell/device-identity.hbs +24 -0
- package/addon/components/cell/device-identity.js +129 -0
- package/addon/components/cell/driver-identity.hbs +33 -0
- package/addon/components/cell/driver-identity.js +143 -0
- package/addon/components/cell/equipment-identity.hbs +5 -0
- package/addon/components/cell/equipment-identity.js +48 -0
- package/addon/components/cell/part-identity.hbs +5 -0
- package/addon/components/cell/part-identity.js +63 -0
- package/addon/components/cell/telematic-device.hbs +38 -0
- package/addon/components/cell/telematic-device.js +44 -0
- package/addon/components/cell/telematic-provider.hbs +33 -16
- package/addon/components/cell/telematic-provider.js +50 -8
- package/addon/components/cell/vehicle-identity.hbs +33 -0
- package/addon/components/cell/vehicle-identity.js +138 -0
- package/addon/components/device/form.hbs +2 -2
- package/addon/components/device/form.js +7 -0
- package/addon/components/device/panel-header.hbs +45 -20
- package/addon/components/device/panel-header.js +49 -1
- package/addon/components/device/panel-tabs/events.hbs +30 -0
- package/addon/components/device/panel-tabs/events.js +128 -0
- package/addon/components/device/panel-tabs/sensors.hbs +30 -0
- package/addon/components/device/panel-tabs/sensors.js +93 -0
- package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
- package/addon/components/device/panel-tabs/vehicle.js +90 -0
- package/addon/components/equipment/form.js +8 -1
- package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +8 -0
- package/addon/components/map/drawer/device-event-listing.js +2 -0
- package/addon/components/map/drawer/driver-listing.js +2 -0
- package/addon/components/map/drawer/place-listing.js +2 -0
- package/addon/components/map/drawer/position-listing.hbs +11 -6
- package/addon/components/map/drawer/position-listing.js +17 -4
- package/addon/components/map/drawer/vehicle-listing.js +2 -0
- package/addon/components/sensor/panel-header.hbs +47 -15
- package/addon/components/sensor/panel-header.js +59 -1
- package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
- package/addon/components/table/cell/dropdown/action-item.js +62 -0
- package/addon/components/table/cell/dropdown.hbs +28 -0
- package/addon/components/table/cell/dropdown.js +91 -0
- package/addon/components/vendor/panel-header.hbs +82 -0
- package/addon/components/vendor/panel-header.js +48 -0
- package/addon/controllers/connectivity/devices/index/details/vehicle.js +49 -1
- package/addon/controllers/connectivity/devices/index.js +144 -129
- package/addon/controllers/connectivity/events/details.js +3 -0
- package/addon/controllers/connectivity/events/index.js +187 -11
- package/addon/controllers/connectivity/sensors/index.js +156 -14
- package/addon/controllers/connectivity/telematics/details/devices.js +126 -134
- package/addon/controllers/maintenance/equipment/index.js +1 -2
- package/addon/controllers/maintenance/parts/index.js +1 -2
- package/addon/controllers/management/drivers/index.js +82 -42
- package/addon/controllers/management/fuel-reports/index.js +63 -4
- package/addon/controllers/management/issues/index.js +63 -4
- package/addon/controllers/management/vehicles/index.js +33 -6
- package/addon/routes/connectivity/devices/index/details/vehicle.js +24 -2
- package/addon/routes/connectivity/devices/index.js +7 -0
- package/addon/routes/connectivity/events/details.js +3 -0
- package/addon/routes/connectivity/events/index.js +6 -2
- package/addon/routes/connectivity/sensors/index.js +7 -2
- package/addon/routes/connectivity/telematics/details/devices.js +2 -0
- package/addon/services/device-actions.js +30 -0
- package/addon/services/device-event-actions.js +1 -1
- package/addon/services/equipment-actions.js +1 -0
- package/addon/services/sensor-actions.js +3 -0
- package/addon/services/service-rate-actions.js +39 -1
- package/addon/services/vehicle-actions.js +24 -12
- package/addon/services/vendor-actions.js +3 -0
- package/addon/styles/fleetops-engine.css +8 -0
- package/addon/templates/connectivity/devices/index/details/vehicle.hbs +64 -1
- package/addon/templates/management/vendors/index/details.hbs +2 -2
- package/addon/templates/settings/notifications.hbs +2 -1
- package/addon/templates/settings/routing.hbs +1 -0
- package/addon/utils/device-table-columns.js +288 -0
- package/addon/utils/identity-cell-resource.js +20 -0
- package/addon/utils/map-drawer-dropdown-position.js +47 -0
- package/addon/utils/trackable-option.js +93 -0
- package/app/components/cell/attached-vehicle.js +1 -0
- package/app/components/cell/device-identity.js +1 -0
- package/app/components/cell/driver-identity.js +1 -0
- package/app/components/cell/equipment-identity.js +1 -0
- package/app/components/cell/part-identity.js +1 -0
- package/app/components/cell/telematic-device.js +1 -0
- package/app/components/cell/vehicle-identity.js +1 -0
- package/app/components/device/panel-tabs/events.hbs +30 -0
- package/app/components/device/panel-tabs/events.js +128 -0
- package/app/components/device/panel-tabs/sensors.hbs +30 -0
- package/app/components/device/panel-tabs/sensors.js +93 -0
- package/app/components/device/panel-tabs/vehicle.hbs +68 -0
- package/app/components/device/panel-tabs/vehicle.js +90 -0
- package/app/components/table/cell/dropdown/action-item.js +1 -0
- package/app/components/table/cell/dropdown.js +1 -0
- package/app/components/vendor/panel-header.js +1 -0
- package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
- package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
- package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
- package/app/utils/map-drawer-dropdown-position.js +1 -0
- package/app/utils/trackable-option.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +4 -4
- package/server/src/Console/Commands/SyncTelematics.php +4 -4
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -1
- package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
- package/server/src/Http/Filter/DeviceEventFilter.php +21 -0
- package/server/src/Http/Filter/DeviceFilter.php +24 -1
- package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
- package/server/src/Models/Device.php +2 -0
- package/server/src/Models/DeviceEvent.php +81 -0
- package/server/src/Models/Equipment.php +55 -0
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +1 -0
- package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
- package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +26 -2
- package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
- package/server/src/routes.php +0 -1
- package/server/tests/DeviceFilterTest.php +9 -0
- package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
- package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
- package/server/tests/ServiceRateRouteValidationTest.php +101 -0
- package/server/tests/TelematicsHardeningTest.php +238 -7
- package/addon/controllers/connectivity/events/index/details.js +0 -3
- package/addon/routes/connectivity/events/index/details.js +0 -3
- /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
@
|
|
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';
|