diff --git a/addon/components/admin/routing-settings.hbs b/addon/components/admin/routing-settings.hbs
index a3a210033..7ee7af45f 100644
--- a/addon/components/admin/routing-settings.hbs
+++ b/addon/components/admin/routing-settings.hbs
@@ -19,6 +19,7 @@
-
\ No newline at end of file
+
diff --git a/addon/components/cell/attached-vehicle.hbs b/addon/components/cell/attached-vehicle.hbs
new file mode 100644
index 000000000..6ad15e212
--- /dev/null
+++ b/addon/components/cell/attached-vehicle.hbs
@@ -0,0 +1,10 @@
+
+ {{#if this.hasVehicle}}
+
+ {{else}}
+
+
+ Unattached
+
+ {{/if}}
+
diff --git a/addon/components/cell/attached-vehicle.js b/addon/components/cell/attached-vehicle.js
new file mode 100644
index 000000000..42c1645a8
--- /dev/null
+++ b/addon/components/cell/attached-vehicle.js
@@ -0,0 +1,58 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+export default class CellAttachedVehicleComponent extends Component {
+ get device() {
+ return this.args.row;
+ }
+
+ get vehicle() {
+ return this.device?.attachable;
+ }
+
+ get vehicleResource() {
+ return (
+ this.vehicle ?? {
+ id: this.device?.attachable_uuid,
+ displayName: this.device?.attached_to_name,
+ display_name: this.device?.attached_to_name,
+ name: this.device?.attached_to_name,
+ public_id: this.device?.attachable_uuid,
+ }
+ );
+ }
+
+ get identityColumn() {
+ return {
+ ...(this.args.column ?? {}),
+ action: null,
+ showStatusBadge: false,
+ };
+ }
+
+ get hasVehicle() {
+ return Boolean(this.device?.attachable_uuid && this.isVehicleAttachment);
+ }
+
+ get isVehicleAttachment() {
+ const attachableType = `${this.device?.attachable_type ?? ''}`.toLowerCase();
+
+ return !attachableType || attachableType.includes('vehicle');
+ }
+
+ @action onClick(_vehicle, event) {
+ const { column, onClick } = this.args;
+
+ if (!this.hasVehicle) {
+ return;
+ }
+
+ if (typeof onClick === 'function') {
+ onClick(this.device, event);
+ }
+
+ if (typeof column?.action === 'function') {
+ column.action(this.device, event);
+ }
+ }
+}
diff --git a/addon/components/cell/device-identity.hbs b/addon/components/cell/device-identity.hbs
new file mode 100644
index 000000000..ddcf5a921
--- /dev/null
+++ b/addon/components/cell/device-identity.hbs
@@ -0,0 +1,24 @@
+{{#if this.resource}}
+ {{#if this.compact}}
+
+
+
+ {{#if this.hasCompactStatusDot}}
+
+ {{/if}}
+
+ {{n-a this.label}}
+
+ {{else}}
+
+ {{/if}}
+{{else}}
+
{{this.emptyText}}
+{{/if}}
diff --git a/addon/components/cell/device-identity.js b/addon/components/cell/device-identity.js
new file mode 100644
index 000000000..4e97068fb
--- /dev/null
+++ b/addon/components/cell/device-identity.js
@@ -0,0 +1,129 @@
+import Component from '@glimmer/component';
+import { action, get } from '@ember/object';
+import config from 'ember-get-config';
+import { resolveIdentityCellResource } from '../../utils/identity-cell-resource';
+
+const DEFAULT_STATUS_TONES = {
+ online: 'text-green-500',
+ active: 'text-green-500',
+ recently_offline: 'text-yellow-500',
+ offline: 'text-gray-400',
+ long_offline: 'text-gray-400',
+ never_connected: 'text-gray-400',
+ inactive: 'text-gray-400',
+ error: 'text-red-500',
+};
+
+export default class CellDeviceIdentityComponent extends Component {
+ get resource() {
+ return resolveIdentityCellResource(this.args);
+ }
+
+ get emptyText() {
+ return this.args.column?.emptyText ?? '-';
+ }
+
+ get showStatus() {
+ return this.args.column?.showStatus ?? true;
+ }
+
+ get compact() {
+ return this.args.column?.compact ?? false;
+ }
+
+ get label() {
+ const device = this.resource;
+
+ return get(device, 'displayName') ?? get(device, 'display_name') ?? get(device, 'name') ?? get(device, 'device_id') ?? get(device, 'imei') ?? get(device, 'serial_number');
+ }
+
+ get mediaUrl() {
+ return get(this.resource, 'photo_url');
+ }
+
+ get fallbackImage() {
+ return config?.defaultValues?.placeholderImage;
+ }
+
+ get hasCompactStatusDot() {
+ return this.args.column?.showStatusDot ?? this.args.column?.showOnlineIndicator ?? true;
+ }
+
+ get compactStatusValue() {
+ const device = this.resource;
+
+ return get(device, 'is_online') ?? get(device, 'connection_status') ?? get(device, 'status');
+ }
+
+ get compactStatusToneClass() {
+ const value = this.compactStatusValue;
+ const statusToneMap = {
+ ...DEFAULT_STATUS_TONES,
+ ...(this.args.column?.statusToneMap ?? {}),
+ };
+
+ if (typeof this.args.column?.statusToneClass === 'function') {
+ return this.args.column.statusToneClass(value, this.resource, this.args.column);
+ }
+
+ if (typeof value === 'boolean') {
+ return value ? 'text-green-500' : 'text-yellow-200';
+ }
+
+ return statusToneMap[value] ?? statusToneMap[String(value ?? '').toLowerCase()] ?? 'text-gray-400';
+ }
+
+ get compactStatusDotClass() {
+ return this.compactStatusToneClass;
+ }
+
+ get column() {
+ return {
+ ...(this.args.column ?? {}),
+ labelPath: (device) =>
+ get(device, 'displayName') ?? get(device, 'display_name') ?? get(device, 'name') ?? get(device, 'device_id') ?? get(device, 'imei') ?? get(device, 'serial_number'),
+ mediaPath: 'photo_url',
+ fallbackImage: config?.defaultValues?.placeholderImage,
+ statusPath: this.showStatus ? (device) => get(device, 'connection_status') ?? get(device, 'status') : undefined,
+ onlinePath: 'is_online',
+ showStatusBadge: this.showStatus ? (this.args.column?.showStatusBadge ?? true) : false,
+ statusBadgeSize: this.args.column?.statusBadgeSize ?? 'xxs',
+ statusBadgeWrapperClass: this.args.column?.statusBadgeWrapperClass ?? 'resource-identity-status-badge device-identity-status-badge',
+ metaPaths: [
+ {
+ value: (device) => get(device, 'imei') ?? get(device, 'device_id') ?? get(device, 'ident') ?? get(device, 'serial_number'),
+ icon: 'microchip',
+ style: 'badge',
+ class: 'max-w-[12rem]',
+ },
+ ],
+ statusToneMap: {
+ online: 'text-green-500',
+ active: 'text-green-500',
+ recently_offline: 'text-yellow-500',
+ offline: 'text-gray-400',
+ long_offline: 'text-gray-400',
+ never_connected: 'text-gray-400',
+ inactive: 'text-gray-400',
+ error: 'text-red-500',
+ },
+ };
+ }
+
+ @action onClick(event) {
+ const { column, onClick } = this.args;
+ const resource = this.resource;
+
+ if (typeof onClick === 'function') {
+ onClick(resource, event);
+ }
+
+ if (typeof column?.onClick === 'function') {
+ column.onClick(resource, event);
+ }
+
+ if (typeof column?.action === 'function') {
+ column.action(resource, event);
+ }
+ }
+}
diff --git a/addon/components/cell/driver-identity.hbs b/addon/components/cell/driver-identity.hbs
new file mode 100644
index 000000000..0b03a862e
--- /dev/null
+++ b/addon/components/cell/driver-identity.hbs
@@ -0,0 +1,33 @@
+{{#if this.resource}}
+ {{#if this.compact}}
+
+
+
+ {{#if this.hasCompactStatusDot}}
+
+ {{/if}}
+
+ {{n-a this.label}}
+ {{#if this.assignedVehicleLabel}}
+
+
+ {{n-a this.assignedVehicleLabel}}
+
+ {{/if}}
+
+ {{else}}
+
+ {{/if}}
+{{else}}
+
{{this.emptyText}}
+{{/if}}
diff --git a/addon/components/cell/driver-identity.js b/addon/components/cell/driver-identity.js
new file mode 100644
index 000000000..8b07e4e94
--- /dev/null
+++ b/addon/components/cell/driver-identity.js
@@ -0,0 +1,143 @@
+import Component from '@glimmer/component';
+import { action, get } from '@ember/object';
+import config from 'ember-get-config';
+import { resolveIdentityCellResource } from '../../utils/identity-cell-resource';
+
+const DEFAULT_STATUS_TONES = {
+ available: 'text-green-500',
+ active: 'text-green-500',
+ on_duty: 'text-green-500',
+ busy: 'text-yellow-500',
+ assigned: 'text-yellow-500',
+ unavailable: 'text-gray-400',
+ offline: 'text-gray-400',
+ suspended: 'text-red-500',
+};
+
+export default class CellDriverIdentityComponent extends Component {
+ get resource() {
+ return resolveIdentityCellResource(this.args);
+ }
+
+ get emptyText() {
+ return this.args.column?.emptyText ?? '-';
+ }
+
+ get compact() {
+ return this.args.column?.compact ?? false;
+ }
+
+ get label() {
+ const driver = this.resource;
+
+ return get(driver, 'name') ?? get(driver, 'displayName') ?? get(driver, 'display_name');
+ }
+
+ get mediaUrl() {
+ return get(this.resource, 'photo_url');
+ }
+
+ get fallbackImage() {
+ return config?.defaultValues?.driverImage;
+ }
+
+ get hasCompactStatusDot() {
+ return this.args.column?.showStatusDot ?? this.args.column?.showOnlineIndicator ?? true;
+ }
+
+ get compactStatusValue() {
+ const driver = this.resource;
+
+ return get(driver, 'online') ?? get(driver, 'status');
+ }
+
+ get compactStatusToneClass() {
+ const value = this.compactStatusValue;
+ const statusToneMap = {
+ ...DEFAULT_STATUS_TONES,
+ ...(this.args.column?.statusToneMap ?? {}),
+ };
+
+ if (typeof this.args.column?.statusToneClass === 'function') {
+ return this.args.column.statusToneClass(value, this.resource, this.args.column);
+ }
+
+ if (typeof value === 'boolean') {
+ return value ? 'text-green-500' : 'text-yellow-200';
+ }
+
+ return statusToneMap[value] ?? statusToneMap[String(value ?? '').toLowerCase()] ?? 'text-gray-400';
+ }
+
+ get compactStatusDotClass() {
+ return this.compactStatusToneClass;
+ }
+
+ get assignedVehicleLabel() {
+ const column = this.args.column ?? {};
+ const driver = this.resource;
+
+ if (typeof column.assignedVehicleLabel === 'function') {
+ return column.assignedVehicleLabel(driver, this.args.row, column);
+ }
+
+ if (column.assignedVehicleLabel !== undefined) {
+ return column.assignedVehicleLabel;
+ }
+
+ if (typeof column.assignedVehiclePath === 'string') {
+ return get(this.args.row, column.assignedVehiclePath) ?? get(driver, column.assignedVehiclePath);
+ }
+
+ return get(driver, 'vehicle_assigned.display_name') ?? get(driver, 'vehicle.display_name') ?? get(driver, 'vehicle_name');
+ }
+
+ get column() {
+ return {
+ ...(this.args.column ?? {}),
+ labelPath: 'name',
+ mediaPath: 'photo_url',
+ fallbackImage: config?.defaultValues?.driverImage,
+ statusPath: 'status',
+ onlinePath: 'online',
+ showStatusBadge: this.args.column?.showStatusBadge ?? true,
+ statusBadgeSize: this.args.column?.statusBadgeSize ?? 'xxs',
+ statusBadgeWrapperClass: this.args.column?.statusBadgeWrapperClass ?? 'resource-identity-status-badge driver-identity-status-badge order-first',
+ metaPaths: [
+ {
+ value: (driver) => get(driver, 'vehicle_assigned.display_name') ?? get(driver, 'vehicle.display_name') ?? get(driver, 'vehicle_name'),
+ icon: 'car',
+ style: 'badge',
+ class: 'max-w-[12rem]',
+ },
+ ],
+ statusToneMap: {
+ available: 'text-green-500',
+ active: 'text-green-500',
+ on_duty: 'text-green-500',
+ busy: 'text-yellow-500',
+ assigned: 'text-yellow-500',
+ unavailable: 'text-gray-400',
+ offline: 'text-gray-400',
+ suspended: 'text-red-500',
+ },
+ };
+ }
+
+ @action onClick(event) {
+ const { column, onClick } = this.args;
+ const resource = this.resource;
+
+ if (typeof onClick === 'function') {
+ onClick(resource, event);
+ }
+
+ if (typeof column?.onClick === 'function') {
+ column.onClick(resource, event);
+ }
+
+ if (typeof column?.action === 'function') {
+ column.action(resource, event);
+ }
+ }
+}
diff --git a/addon/components/cell/equipment-identity.hbs b/addon/components/cell/equipment-identity.hbs
new file mode 100644
index 000000000..4755c976c
--- /dev/null
+++ b/addon/components/cell/equipment-identity.hbs
@@ -0,0 +1,5 @@
+{{#if this.resource}}
+
+{{else}}
+
{{this.emptyText}}
+{{/if}}
diff --git a/addon/components/cell/equipment-identity.js b/addon/components/cell/equipment-identity.js
new file mode 100644
index 000000000..8c1e5b6b8
--- /dev/null
+++ b/addon/components/cell/equipment-identity.js
@@ -0,0 +1,48 @@
+import Component from '@glimmer/component';
+import { get } from '@ember/object';
+import config from 'ember-get-config';
+import { resolveIdentityCellResource } from '../../utils/identity-cell-resource';
+
+export default class CellEquipmentIdentityComponent extends Component {
+ get resource() {
+ return resolveIdentityCellResource(this.args);
+ }
+
+ get emptyText() {
+ return this.args.column?.emptyText ?? '-';
+ }
+
+ get column() {
+ return {
+ ...(this.args.column ?? {}),
+ labelPath: 'name',
+ mediaPath: 'photo_url',
+ fallbackImage: config?.defaultValues?.equipmentImage ?? config?.defaultValues?.placeholderImage,
+ statusPath: (equipment) => (get(equipment, 'is_equipped') ? 'equipped' : (get(equipment, 'status') ?? 'unequipped')),
+ statusFormatter: () => null,
+ metaPaths: [
+ {
+ value: (equipment) => get(equipment, 'type'),
+ formatter: (type) => type,
+ icon: 'toolbox',
+ style: 'badge',
+ },
+ {
+ value: (equipment) => get(equipment, 'serial_number') ?? get(equipment, 'code') ?? get(equipment, 'public_id'),
+ icon: 'barcode',
+ style: 'badge',
+ class: 'max-w-[12rem]',
+ },
+ ],
+ statusToneMap: {
+ active: 'text-green-500',
+ equipped: 'text-green-500',
+ available: 'text-green-500',
+ maintenance: 'text-yellow-500',
+ unequipped: 'text-gray-400',
+ inactive: 'text-gray-400',
+ retired: 'text-red-500',
+ },
+ };
+ }
+}
diff --git a/addon/components/cell/part-identity.hbs b/addon/components/cell/part-identity.hbs
new file mode 100644
index 000000000..4755c976c
--- /dev/null
+++ b/addon/components/cell/part-identity.hbs
@@ -0,0 +1,5 @@
+{{#if this.resource}}
+
+{{else}}
+
{{this.emptyText}}
+{{/if}}
diff --git a/addon/components/cell/part-identity.js b/addon/components/cell/part-identity.js
new file mode 100644
index 000000000..e0ff3de1d
--- /dev/null
+++ b/addon/components/cell/part-identity.js
@@ -0,0 +1,63 @@
+import Component from '@glimmer/component';
+import { get } from '@ember/object';
+import config from 'ember-get-config';
+import { resolveIdentityCellResource } from '../../utils/identity-cell-resource';
+
+function inventoryStatus(part) {
+ if (get(part, 'is_low_stock')) {
+ return 'low_stock';
+ }
+
+ if (get(part, 'is_in_stock')) {
+ return 'in_stock';
+ }
+
+ return 'out_of_stock';
+}
+
+function inventoryStatusLabel(part) {
+ return inventoryStatus(part)
+ .split('_')
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ');
+}
+
+export default class CellPartIdentityComponent extends Component {
+ get resource() {
+ return resolveIdentityCellResource(this.args);
+ }
+
+ get emptyText() {
+ return this.args.column?.emptyText ?? '-';
+ }
+
+ get column() {
+ return {
+ ...(this.args.column ?? {}),
+ labelPath: 'name',
+ mediaPath: 'photo_url',
+ fallbackImage: config?.defaultValues?.partImage ?? config?.defaultValues?.placeholderImage,
+ statusPath: inventoryStatus,
+ statusFormatter: () => null,
+ metaPaths: [
+ {
+ value: (part) => get(part, 'type'),
+ icon: 'tag',
+ style: 'badge',
+ },
+ {
+ value: inventoryStatusLabel,
+ icon: 'boxes-stacked',
+ style: 'badge',
+ },
+ ],
+ statusToneMap: {
+ in_stock: 'text-green-500',
+ low_stock: 'text-yellow-500',
+ out_of_stock: 'text-red-500',
+ active: 'text-green-500',
+ inactive: 'text-gray-400',
+ },
+ };
+ }
+}
diff --git a/addon/components/cell/telematic-device.hbs b/addon/components/cell/telematic-device.hbs
new file mode 100644
index 000000000..2497b1f57
--- /dev/null
+++ b/addon/components/cell/telematic-device.hbs
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
{{n-a this.name}}
+
+
+ {{smart-humanize this.connectionStatus}}
+
+ {{n-a this.identifier}}
+
+
+
+
diff --git a/addon/components/cell/telematic-device.js b/addon/components/cell/telematic-device.js
new file mode 100644
index 000000000..1627ab284
--- /dev/null
+++ b/addon/components/cell/telematic-device.js
@@ -0,0 +1,44 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+export default class CellTelematicDeviceComponent extends Component {
+ get device() {
+ return this.args.row;
+ }
+
+ get name() {
+ return this.device?.displayName ?? this.device?.display_name ?? this.device?.name ?? this.device?.device_id ?? this.device?.imei ?? this.device?.serial_number;
+ }
+
+ get identifier() {
+ return this.device?.imei ?? this.device?.device_id ?? this.device?.internal_id ?? this.device?.serial_number ?? this.device?.public_id;
+ }
+
+ get imageUrl() {
+ return this.device?.photo_url;
+ }
+
+ get connectionStatus() {
+ return this.device?.connection_status ?? (this.device?.is_online ? 'online' : 'offline');
+ }
+
+ get isOnline() {
+ return this.device?.is_online || this.connectionStatus === 'online';
+ }
+
+ @action onClick(event) {
+ const { column, onClick } = this.args;
+
+ if (typeof onClick === 'function') {
+ onClick(this.device, event);
+ }
+
+ if (typeof column?.action === 'function') {
+ column.action(this.device, event);
+ }
+
+ if (typeof column?.onClick === 'function') {
+ column.onClick(this.device, event);
+ }
+ }
+}
diff --git a/addon/components/cell/telematic-provider.hbs b/addon/components/cell/telematic-provider.hbs
index 9897c5591..462f0610f 100644
--- a/addon/components/cell/telematic-provider.hbs
+++ b/addon/components/cell/telematic-provider.hbs
@@ -1,16 +1,33 @@
-
-
-
-
-
-
-
{{n-a this.name}}
-
{{n-a this.description}}
-
-
-
+{{#if this.telematic}}
+ {{#if this.compact}}
+
+
+ {{n-a this.name}}
+
+ {{else}}
+
+
+
+
+
+
{{n-a this.name}}
+
{{n-a this.description}}
+
+
+ {{/if}}
+{{else}}
+
{{this.emptyText}}
+{{/if}}
diff --git a/addon/components/cell/telematic-provider.js b/addon/components/cell/telematic-provider.js
index 0aef0f697..3d6213a36 100644
--- a/addon/components/cell/telematic-provider.js
+++ b/addon/components/cell/telematic-provider.js
@@ -1,36 +1,78 @@
import Component from '@glimmer/component';
-import { action } from '@ember/object';
+import { action, get } from '@ember/object';
+
+const DEFAULT_PROVIDER_ICON = '/engines-dist/images/telematics/providers/default.webp';
export default class CellTelematicProviderComponent extends Component {
+ get telematic() {
+ const resourcePath = this.args.column?.resourcePath;
+
+ if (typeof resourcePath === 'function') {
+ const resource = resourcePath(this.args.row, this.args.value, this.args.column);
+
+ return resource ?? null;
+ }
+
+ if (typeof resourcePath === 'string') {
+ const resource = get(this.args.row, resourcePath);
+
+ return resource ?? null;
+ }
+
+ if (this.args.row?.telematic) {
+ return this.args.row.telematic;
+ }
+
+ if (this.args.row?.telematic_uuid || this.args.row?.telematic_name) {
+ return {
+ id: this.args.row.telematic_uuid,
+ name: this.args.row.telematic_name,
+ provider: this.args.row.provider,
+ provider_descriptor: this.args.row.provider_descriptor,
+ };
+ }
+
+ return this.args.row;
+ }
+
+ get emptyText() {
+ return this.args.column?.emptyText ?? '-';
+ }
+
+ get compact() {
+ return this.args.column?.compact ?? false;
+ }
+
get descriptor() {
- return this.args.row?.provider_descriptor ?? {};
+ return this.telematic?.provider_descriptor ?? this.args.row?.provider_descriptor ?? {};
}
get name() {
- return this.args.row?.name ?? this.descriptor.label ?? this.args.row?.provider;
+ return this.telematic?.name ?? this.descriptor.label ?? this.telematic?.provider ?? this.args.row?.provider ?? this.args.row?.telematic_name;
}
get description() {
- return this.descriptor.description ?? this.args.row?.provider;
+ return this.descriptor.description ?? this.telematic?.provider ?? this.args.row?.provider;
}
get icon() {
- return this.descriptor.icon;
+ return this.descriptor.icon ?? DEFAULT_PROVIDER_ICON;
}
@action onClick(event) {
const { row, column, onClick } = this.args;
+ const resource = this.telematic ?? row;
if (typeof onClick === 'function') {
- onClick(row, event);
+ onClick(resource, event);
}
if (typeof column?.action === 'function') {
- column.action(row, event);
+ column.action(resource, event);
}
if (typeof column?.onClick === 'function') {
- column.onClick(row, event);
+ column.onClick(resource, event);
}
}
}
diff --git a/addon/components/cell/vehicle-identity.hbs b/addon/components/cell/vehicle-identity.hbs
new file mode 100644
index 000000000..6abea09e9
--- /dev/null
+++ b/addon/components/cell/vehicle-identity.hbs
@@ -0,0 +1,33 @@
+{{#if this.resource}}
+ {{#if this.compact}}
+
+
+
+ {{#if this.hasCompactStatusDot}}
+
+ {{/if}}
+
+ {{n-a this.label}}
+ {{#if this.assignedDriverLabel}}
+
+
+ {{n-a this.assignedDriverLabel}}
+
+ {{/if}}
+
+ {{else}}
+
+ {{/if}}
+{{else}}
+
{{this.emptyText}}
+{{/if}}
diff --git a/addon/components/cell/vehicle-identity.js b/addon/components/cell/vehicle-identity.js
new file mode 100644
index 000000000..f1724abeb
--- /dev/null
+++ b/addon/components/cell/vehicle-identity.js
@@ -0,0 +1,138 @@
+import Component from '@glimmer/component';
+import { action, get } from '@ember/object';
+import config from 'ember-get-config';
+import { resolveIdentityCellResource } from '../../utils/identity-cell-resource';
+
+const DEFAULT_STATUS_TONES = {
+ available: 'text-green-500',
+ active: 'text-green-500',
+ in_service: 'text-green-500',
+ maintenance: 'text-yellow-500',
+ unavailable: 'text-gray-400',
+ inactive: 'text-gray-400',
+ out_of_service: 'text-red-500',
+};
+
+export default class CellVehicleIdentityComponent extends Component {
+ get resource() {
+ return resolveIdentityCellResource(this.args);
+ }
+
+ get emptyText() {
+ return this.args.column?.emptyText ?? '-';
+ }
+
+ get compact() {
+ return this.args.column?.compact ?? false;
+ }
+
+ get showStatus() {
+ return this.args.column?.showStatus ?? true;
+ }
+
+ get label() {
+ const vehicle = this.resource;
+
+ return get(vehicle, 'displayName') ?? get(vehicle, 'display_name') ?? get(vehicle, 'name');
+ }
+
+ get mediaUrl() {
+ return get(this.resource, 'photo_url');
+ }
+
+ get fallbackImage() {
+ return config?.defaultValues?.vehicleAvatar;
+ }
+
+ get hasCompactStatusDot() {
+ return this.args.column?.showStatusDot ?? this.args.column?.showOnlineIndicator ?? true;
+ }
+
+ get compactStatusValue() {
+ const vehicle = this.resource;
+
+ return get(vehicle, 'online') ?? get(vehicle, 'status');
+ }
+
+ get compactStatusToneClass() {
+ const value = this.compactStatusValue;
+ const statusToneMap = {
+ ...DEFAULT_STATUS_TONES,
+ ...(this.args.column?.statusToneMap ?? {}),
+ };
+
+ if (typeof this.args.column?.statusToneClass === 'function') {
+ return this.args.column.statusToneClass(value, this.resource, this.args.column);
+ }
+
+ if (typeof value === 'boolean') {
+ return value ? 'text-green-500' : 'text-yellow-200';
+ }
+
+ return statusToneMap[value] ?? statusToneMap[String(value ?? '').toLowerCase()] ?? 'text-gray-400';
+ }
+
+ get compactStatusDotClass() {
+ return this.compactStatusToneClass;
+ }
+
+ get assignedDriverLabel() {
+ const vehicle = this.resource;
+
+ return get(vehicle, 'driver.displayName') ?? get(vehicle, 'driver.display_name') ?? get(vehicle, 'driver.name') ?? get(vehicle, 'driver_name');
+ }
+
+ get column() {
+ return {
+ ...(this.args.column ?? {}),
+ labelPath: (vehicle) => get(vehicle, 'displayName') ?? get(vehicle, 'display_name') ?? get(vehicle, 'name'),
+ mediaPath: 'photo_url',
+ fallbackImage: config?.defaultValues?.vehicleAvatar,
+ statusPath: this.showStatus ? 'status' : undefined,
+ onlinePath: 'online',
+ showStatusBadge: this.showStatus ? (this.args.column?.showStatusBadge ?? true) : false,
+ statusBadgeSize: this.args.column?.statusBadgeSize ?? 'xxs',
+ statusBadgeWrapperClass: this.args.column?.statusBadgeWrapperClass ?? 'resource-identity-status-badge vehicle-identity-status-badge',
+ metaPaths: [
+ {
+ value: (vehicle) => get(vehicle, 'plate_number') ?? get(vehicle, 'call_sign') ?? get(vehicle, 'vehicle_number') ?? get(vehicle, 'public_id'),
+ icon: 'id-card',
+ style: 'badge',
+ class: 'max-w-[12rem]',
+ },
+ {
+ value: (vehicle) => get(vehicle, 'driver.displayName') ?? get(vehicle, 'driver.display_name') ?? get(vehicle, 'driver.name') ?? get(vehicle, 'driver_name'),
+ icon: 'user',
+ style: 'badge',
+ class: 'max-w-[12rem]',
+ },
+ ],
+ statusToneMap: {
+ available: 'text-green-500',
+ active: 'text-green-500',
+ in_service: 'text-green-500',
+ maintenance: 'text-yellow-500',
+ unavailable: 'text-gray-400',
+ inactive: 'text-gray-400',
+ out_of_service: 'text-red-500',
+ },
+ };
+ }
+
+ @action onClick(event) {
+ const { column, onClick } = this.args;
+ const resource = this.resource;
+
+ if (typeof onClick === 'function') {
+ onClick(resource, event);
+ }
+
+ if (typeof column?.onClick === 'function') {
+ column.onClick(resource, event);
+ }
+
+ if (typeof column?.action === 'function') {
+ column.action(resource, event);
+ }
+ }
+}
diff --git a/addon/components/device/form.hbs b/addon/components/device/form.hbs
index 6da0167bf..93936107c 100644
--- a/addon/components/device/form.hbs
+++ b/addon/components/device/form.hbs
@@ -10,7 +10,7 @@
@infiniteScroll={{false}}
@renderInPlace={{true}}
@onChange={{this.selectTelematic}}
- @disabled={{cannot-write @resource}}
+ @disabled={{or (cannot-write @resource) this.isTelematicLocked}}
as |model|
>
@@ -227,4 +227,4 @@
-
\ No newline at end of file
+
diff --git a/addon/components/device/form.js b/addon/components/device/form.js
index 0a7008634..cade1d20f 100644
--- a/addon/components/device/form.js
+++ b/addon/components/device/form.js
@@ -8,6 +8,13 @@ export default class DeviceFormComponent extends Component {
@service currentUser;
@service notifications;
+ get isTelematicLocked() {
+ const resource = this.args.resource;
+ const hasTelematic = Boolean(resource?.telematic_uuid || resource?.telematic?.id);
+
+ return Boolean(hasTelematic && resource?.isNew === false);
+ }
+
@action selectTelematic(telematic) {
this.args.resource.setProperties({
telematic,
diff --git a/addon/components/device/panel-header.hbs b/addon/components/device/panel-header.hbs
index 73613520c..d7ac26ab3 100644
--- a/addon/components/device/panel-header.hbs
+++ b/addon/components/device/panel-header.hbs
@@ -1,35 +1,60 @@
-