@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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { action } from '@ember/object';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
import { tracked } from '@glimmer/tracking';
|
|
5
|
+
import { task } from 'ember-concurrency';
|
|
6
|
+
|
|
7
|
+
function toRecentList(records) {
|
|
8
|
+
const list = Array.from(records ?? []);
|
|
9
|
+
|
|
10
|
+
list.meta = {
|
|
11
|
+
current_page: 1,
|
|
12
|
+
last_page: 1,
|
|
13
|
+
per_page: list.length,
|
|
14
|
+
total: list.length,
|
|
15
|
+
from: list.length > 0 ? 1 : 0,
|
|
16
|
+
to: list.length,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return list;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default class DevicePanelTabsSensorsComponent extends Component {
|
|
23
|
+
@service sensorActions;
|
|
24
|
+
@service store;
|
|
25
|
+
|
|
26
|
+
@tracked sensors = toRecentList();
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
super(...arguments);
|
|
30
|
+
this.loadSensors.perform();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get device() {
|
|
34
|
+
return this.args.resource ?? this.args.model;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get columns() {
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
sticky: true,
|
|
41
|
+
label: 'Sensor',
|
|
42
|
+
valuePath: 'name',
|
|
43
|
+
cellComponent: 'table/cell/anchor',
|
|
44
|
+
action: this.sensorActions.panel?.view ?? this.sensorActions.transition.view,
|
|
45
|
+
permission: 'fleet-ops view sensor',
|
|
46
|
+
resizable: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: 'Type',
|
|
50
|
+
valuePath: 'type',
|
|
51
|
+
cellComponent: 'table/cell/base',
|
|
52
|
+
humanize: true,
|
|
53
|
+
resizable: true,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: 'Value',
|
|
57
|
+
valuePath: 'last_value',
|
|
58
|
+
resizable: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
label: 'Unit',
|
|
62
|
+
valuePath: 'unit',
|
|
63
|
+
resizable: true,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: 'Status',
|
|
67
|
+
valuePath: 'status',
|
|
68
|
+
cellComponent: 'table/cell/status',
|
|
69
|
+
resizable: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: 'Last Reading',
|
|
73
|
+
valuePath: 'lastReadingAt',
|
|
74
|
+
sortParam: 'last_reading_at',
|
|
75
|
+
resizable: true,
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@action refreshSensors() {
|
|
81
|
+
return this.loadSensors.perform();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@task *loadSensors() {
|
|
85
|
+
if (!this.device?.id) {
|
|
86
|
+
this.sensors = toRecentList();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const sensors = yield this.store.query('sensor', { device_uuid: this.device.id, limit: 10, sort: '-updated_at' });
|
|
91
|
+
this.sensors = toRecentList(sensors);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -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
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
@primaryAction={{this.createOrderAction}}
|
|
4
4
|
@searchPlaceholder="Search Fleet-Ops..."
|
|
5
5
|
@searchProvider={{this.searchNavigation}}
|
|
6
|
-
@
|
|
6
|
+
@shouldSyncInitialActiveParent={{this.shouldSyncInitialActiveParent}}
|
|
7
7
|
class="fleet-ops-sidebar-navigator"
|
|
8
8
|
>
|
|
9
9
|
<:footer as |state|>
|
|
@@ -390,6 +390,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
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
|
+
|
|
393
401
|
@action
|
|
394
402
|
async searchNavigation({ query, limit = 12 }) {
|
|
395
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.
|
|
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
|
|
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">{{
|
|
23
|
-
<div class="hide-from-trigger text-xs">{{
|
|
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(
|
|
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
|
{
|
|
@@ -1,29 +1,61 @@
|
|
|
1
|
-
<div class="px-4 py-
|
|
2
|
-
<div class="flex flex-1 flex-row items-start justify-between">
|
|
3
|
-
<div class="flex flex-row
|
|
4
|
-
<div class="flex items-start justify-start
|
|
1
|
+
<div class="px-4 py-3" ...attributes>
|
|
2
|
+
<div class="flex flex-1 flex-row items-start justify-between gap-4">
|
|
3
|
+
<div class="flex min-w-0 flex-row items-start gap-3">
|
|
4
|
+
<div class="relative flex flex-shrink-0 items-start justify-start">
|
|
5
5
|
<Image
|
|
6
6
|
src={{@resource.photo_url}}
|
|
7
7
|
@fallbackSrc={{config "defaultValues.placeholderImage"}}
|
|
8
|
-
alt={{
|
|
8
|
+
alt={{this.name}}
|
|
9
9
|
height="48"
|
|
10
10
|
width="48"
|
|
11
|
-
class="h-14 w-14 rounded-
|
|
11
|
+
class="h-14 w-14 rounded-md border border-gray-200 object-cover shadow-sm dark:border-gray-700"
|
|
12
12
|
/>
|
|
13
13
|
<Attach::Tooltip @class="clean" @animation="scale" @placement="top">
|
|
14
|
-
<InputInfo @text={{
|
|
14
|
+
<InputInfo @text={{this.name}} />
|
|
15
15
|
</Attach::Tooltip>
|
|
16
16
|
</div>
|
|
17
|
-
<div class="flex flex-col">
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
17
|
+
<div class="flex min-w-0 flex-col">
|
|
18
|
+
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
|
19
|
+
<h1 class="truncate text-base font-semibold leading-5 text-gray-900 dark:text-gray-100">{{this.name}}</h1>
|
|
20
|
+
<Badge @status={{this.status}} @size="xs">{{smart-humanize this.status}}</Badge>
|
|
21
|
+
{{#if this.thresholdStatus}}
|
|
22
|
+
<Badge @status={{if (eq this.thresholdStatus "normal") "success" "warning"}} @size="xs">{{smart-humanize this.thresholdStatus}}</Badge>
|
|
23
|
+
{{/if}}
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="mt-1 flex max-w-2xl flex-wrap items-center gap-1.5">
|
|
27
|
+
{{#if this.type}}
|
|
28
|
+
<span class="inline-flex max-w-[10rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
|
|
29
|
+
<FaIcon @icon="gauge-high" class="text-gray-400 dark:text-gray-300" />
|
|
30
|
+
<span class="truncate">{{n-a (get-fleet-ops-option-label "sensorTypes" this.type)}}</span>
|
|
31
|
+
</span>
|
|
32
|
+
{{/if}}
|
|
33
|
+
{{#if this.identifier}}
|
|
34
|
+
<span class="inline-flex max-w-[12rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
|
|
35
|
+
<FaIcon @icon="id-card" class="text-gray-400 dark:text-gray-300" />
|
|
36
|
+
<span class="truncate">{{this.identifier}}</span>
|
|
37
|
+
</span>
|
|
38
|
+
{{/if}}
|
|
39
|
+
{{#if this.reading}}
|
|
40
|
+
<span class="inline-flex max-w-[10rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
|
|
41
|
+
<FaIcon @icon="chart-line" class="text-gray-400 dark:text-gray-300" />
|
|
42
|
+
<span class="truncate">{{this.reading}}</span>
|
|
43
|
+
</span>
|
|
44
|
+
{{/if}}
|
|
45
|
+
{{#if this.deviceName}}
|
|
46
|
+
<span class="inline-flex max-w-[12rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
|
|
47
|
+
<FaIcon @icon="microchip" class="text-gray-400 dark:text-gray-300" />
|
|
48
|
+
<span class="truncate">{{this.deviceName}}</span>
|
|
49
|
+
</span>
|
|
50
|
+
{{/if}}
|
|
51
|
+
<span class="inline-flex max-w-[13rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
|
|
52
|
+
<FaIcon @icon="clock" class="text-gray-400 dark:text-gray-300" />
|
|
53
|
+
<span class="truncate">{{#if this.lastReadingAt}}{{format-date-fns this.lastReadingAt "dd MMM yyyy, HH:mm"}}{{else}}No reading yet{{/if}}</span>
|
|
54
|
+
</span>
|
|
22
55
|
</div>
|
|
23
|
-
<Badge @status={{if @resource.online "online" "offline"}}>{{if @resource.online (t "common.online") (t "common.offline")}}</Badge>
|
|
24
56
|
</div>
|
|
25
57
|
</div>
|
|
26
|
-
<div class="next-view-header-right">
|
|
58
|
+
<div class="next-view-header-right flex-shrink-0">
|
|
27
59
|
<Layout::Resource::Panel::HeaderActions
|
|
28
60
|
@resource={{@resource}}
|
|
29
61
|
@saveTask={{@saveTask}}
|
|
@@ -36,4 +68,4 @@
|
|
|
36
68
|
/>
|
|
37
69
|
</div>
|
|
38
70
|
</div>
|
|
39
|
-
</div>
|
|
71
|
+
</div>
|
|
@@ -1,3 +1,61 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
|
+
import { get } from '@ember/object';
|
|
2
3
|
|
|
3
|
-
export default class SensorPanelHeaderComponent extends Component {
|
|
4
|
+
export default class SensorPanelHeaderComponent extends Component {
|
|
5
|
+
get resource() {
|
|
6
|
+
return this.args.resource;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get name() {
|
|
10
|
+
return (
|
|
11
|
+
get(this.resource, 'displayName') ??
|
|
12
|
+
get(this.resource, 'display_name') ??
|
|
13
|
+
get(this.resource, 'name') ??
|
|
14
|
+
get(this.resource, 'serial_number') ??
|
|
15
|
+
get(this.resource, 'imei') ??
|
|
16
|
+
get(this.resource, 'public_id') ??
|
|
17
|
+
'-'
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get status() {
|
|
22
|
+
return get(this.resource, 'status') ?? (get(this.resource, 'is_active') ? 'active' : 'inactive');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get thresholdStatus() {
|
|
26
|
+
return get(this.resource, 'threshold_status');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get type() {
|
|
30
|
+
return get(this.resource, 'type');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get identifier() {
|
|
34
|
+
return get(this.resource, 'serial_number') ?? get(this.resource, 'imei') ?? get(this.resource, 'internal_id') ?? get(this.resource, 'public_id');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get reading() {
|
|
38
|
+
const value = get(this.resource, 'last_value');
|
|
39
|
+
const unit = get(this.resource, 'unit');
|
|
40
|
+
|
|
41
|
+
if (value === null || value === undefined || value === '') {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return unit ? `${value} ${unit}` : String(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get deviceName() {
|
|
49
|
+
return (
|
|
50
|
+
get(this.resource, 'device.displayName') ??
|
|
51
|
+
get(this.resource, 'device.display_name') ??
|
|
52
|
+
get(this.resource, 'device.name') ??
|
|
53
|
+
get(this.resource, 'device_name') ??
|
|
54
|
+
get(this.resource, 'device_uuid')
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get lastReadingAt() {
|
|
59
|
+
return get(this.resource, 'last_reading_at') ?? get(this.resource, 'lastReadingAt');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{{#if this.visible}}
|
|
2
|
+
{{#if @columnAction.separator}}
|
|
3
|
+
<div class="next-dd-menu-seperator"></div>
|
|
4
|
+
{{else}}
|
|
5
|
+
<div role="group" class="px-1">
|
|
6
|
+
<a
|
|
7
|
+
href="javascript:;"
|
|
8
|
+
role="menuitem"
|
|
9
|
+
class="next-dd-item {{@columnAction.class}} {{if this.disabled 'disabled'}}"
|
|
10
|
+
disabled={{this.disabled}}
|
|
11
|
+
{{on "click" (fn this.onClick @columnAction @row @dd)}}
|
|
12
|
+
>
|
|
13
|
+
{{#if @columnAction.icon}}
|
|
14
|
+
<div class="w-6">
|
|
15
|
+
<FaIcon class={{@columnAction.iconClass}} @icon={{@columnAction.icon}} @prefix={{@columnAction.iconPrefix}} />
|
|
16
|
+
</div>
|
|
17
|
+
{{/if}}
|
|
18
|
+
{{@columnAction.label}}
|
|
19
|
+
{{#if this.doesntHavePermissions}}
|
|
20
|
+
<Attach::Tooltip @class="clean" @animation="scale" @placement={{or @columnAction.tooltipPlacement "right"}}>
|
|
21
|
+
<InputInfo @text={{t "common.unauthorized"}} />
|
|
22
|
+
</Attach::Tooltip>
|
|
23
|
+
{{else if @columnAction.helpText}}
|
|
24
|
+
<Attach::Tooltip @class="clean" @animation="scale" @placement={{or @columnAction.tooltipPlacement "right"}}>
|
|
25
|
+
<InputInfo @text={{@columnAction.helpText}} @exampleText={{@columnAction.exampleText}} />
|
|
26
|
+
</Attach::Tooltip>
|
|
27
|
+
{{/if}}
|
|
28
|
+
</a>
|
|
29
|
+
</div>
|
|
30
|
+
{{/if}}
|
|
31
|
+
{{/if}}
|