Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/postman.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ jobs:
# published image, not the branch under review. The workflow checks this
# repository out at the commit under test and swaps it into the container.
overlay-package: fleetbase/fleetops-api
# This coordinated PR adds the Trailer lifecycle to fleetbase/postman#58.
# Exercise that immutable collection revision during PR review; once merged,
# normal main-branch runs continue following the Postman main branch.
postman-ref: ${{ github.event_name == 'pull_request' && '0590548fefea68dd873b04f67d3f598c401d3af7' || 'main' }}
secrets: inherit
2 changes: 1 addition & 1 deletion addon/components/cell/attached-vehicle.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{{else}}
<div class="flex items-center gap-2 py-1 text-xs text-gray-400 dark:text-gray-500">
<FaIcon @icon="link-slash" />
<span>Unattached</span>
<span>{{t "trailer.attachment.detached"}}</span>
</div>
{{/if}}
</div>
6 changes: 3 additions & 3 deletions addon/components/cell/attached-vehicle.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ export default class CellAttachedVehicleComponent extends Component {
}

get hasVehicle() {
return Boolean(this.device?.attachable_uuid && this.isVehicleAttachment);
return Boolean(this.device?.attachable_uuid && this.isSupportedAttachment);
}

get isVehicleAttachment() {
get isSupportedAttachment() {
const attachableType = `${this.device?.attachable_type ?? ''}`.toLowerCase();

return !attachableType || attachableType.includes('vehicle');
return !attachableType || attachableType.includes('vehicle') || attachableType.includes('trailer');
}

@action onClick(_vehicle, event) {
Expand Down
9 changes: 9 additions & 0 deletions addon/components/cell/translated-value.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{{#if this.label}}
{{#if this.isBadge}}
<Badge @status={{this.value}} @text={{this.label}} @icon={{this.icon}} @hideStatusDot={{unless this.icon false true}} ...attributes />
{{else}}
<span class={{@column.className}} ...attributes>{{this.label}}</span>
{{/if}}
{{else}}
<span class="text-gray-400 dark:text-gray-500" ...attributes>{{this.emptyText}}</span>
{{/if}}
57 changes: 57 additions & 0 deletions addon/components/cell/translated-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { get } from '@ember/object';

/**
* Table cell that renders a stable enum value (`dry_van`, `never_connected`, ...)
* through its localized label. Configure the column with:
*
* translationPrefix: 'trailer.types' // required, joined with the raw value
* badge: true // optional, render as a status badge
* badgeIconPath / badgeIcons // optional badge icon per value
*/
export default class CellTranslatedValueComponent extends Component {
@service intl;

get value() {
const row = this.args.row;
const valuePath = this.args.column?.valuePath;

if (this.args.value !== undefined && this.args.value !== null) {
return this.args.value;
}

return valuePath ? get(row, valuePath) : null;
}

get label() {
const value = this.value;
const prefix = this.args.column?.translationPrefix;

if (value === null || value === undefined || value === '') {
return null;
}

if (!prefix) {
return String(value);
}

const key = `${prefix}.${value}`;

return this.intl.exists(key) ? this.intl.t(key) : String(value);
}

get isBadge() {
return Boolean(this.args.column?.badge);
}

get icon() {
const icons = this.args.column?.badgeIcons;

return icons && this.value ? icons[this.value] : undefined;
}

get emptyText() {
return this.args.column?.emptyText ?? '-';
}
}
2 changes: 1 addition & 1 deletion addon/components/device/manager.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<div class="flex flex-row justify-between px-3 py-4 border-t border-gray-200 dark:border-gray-700">
<Device::Pill @device={{device}} />
<div>
<Button @type="danger" @icon="times" @text={{t "device.actions.detach-from-vehicle"}} @size="xs" @onClick={{fn this.removeDevice device}} />
<Button @type="danger" @icon="times" @text={{t "device.attachment.detach"}} @size="xs" @onClick={{fn this.removeDevice device}} />
</div>
</div>
{{else}}
Expand Down
21 changes: 19 additions & 2 deletions addon/components/device/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { inject as service } from '@ember/service';
import { debug } from '@ember/debug';
import { task } from 'ember-concurrency';
import getModelName from '@fleetbase/ember-core/utils/get-model-name';
import { pluralize } from 'ember-inflector';
import { dasherize } from '@ember/string';

export default class DeviceManagerComponent extends Component {
@service store;
Expand All @@ -28,6 +30,21 @@ export default class DeviceManagerComponent extends Component {
);
}

/**
* The internal attach/detach endpoints are namespaced by the attachable resource
* (`vehicles/{id}/attach-device`, `trailers/{id}/attach-device`). Derive the prefix
* from the resource's model name unless an explicit `@endpoint` is given.
*/
get endpoint() {
if (this.args.endpoint) {
return this.args.endpoint;
}

const modelName = getModelName(this.args.resource);

return modelName ? dasherize(pluralize(modelName)) : 'vehicles';
}

constructor() {
super(...arguments);
this.loadDevices.perform();
Expand All @@ -45,7 +62,7 @@ export default class DeviceManagerComponent extends Component {
modal.startLoading();

try {
await this.fetch.post(`vehicles/${this.args.resource.id}/attach-device`, { device: selectedDevice.id });
await this.fetch.post(`${this.endpoint}/${this.args.resource.id}/attach-device`, { device: selectedDevice.id });
await this.loadDevices.perform();
this.notifications.success(this.intl.t('device.prompts.attach-device-success'));
modal.done();
Expand All @@ -67,7 +84,7 @@ export default class DeviceManagerComponent extends Component {
modal.startLoading();

try {
await this.fetch.post(`vehicles/${this.args.resource.id}/detach-device`, { device: device.id });
await this.fetch.post(`${this.endpoint}/${this.args.resource.id}/detach-device`, { device: device.id });
await this.loadDevices.perform();
this.notifications.success(this.intl.t('device.prompts.detach-from-resource-success', { deviceName, resourceName: this.resourceName }));
modal.done();
Expand Down
26 changes: 13 additions & 13 deletions addon/components/device/panel-tabs/vehicle.hbs
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<div class="flex flex-col gap-3 p-3" ...attributes>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Vehicle Attachment</h2>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">Fleet context for telemetry from this device.</p>
<h2 class="text-base font-bold text-gray-900 dark:text-gray-100">{{t "device.attachment.title"}}</h2>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">{{t "device.attachment.description"}}</p>
</div>
<div class="flex flex-wrap items-center justify-end gap-2">
{{#if this.hasVehicle}}
{{#if this.canOpenVehicle}}
<Button @icon="eye" @text="Open Vehicle" @size="xs" @onClick={{this.openVehicle}} />
<Button @icon="eye" @text={{t "device.attachment.open"}} @size="xs" @onClick={{this.openVehicle}} />
{{/if}}
{{#if this.canLocateVehicle}}
<Button @icon="location-dot" @text="Locate" @size="xs" @onClick={{this.locateVehicle}} />
<Button @icon="location-dot" @text={{t "device.attachment.locate"}} @size="xs" @onClick={{this.locateVehicle}} />
{{/if}}
<Button @icon="shuffle" @text="Change Vehicle" @size="xs" @onClick={{this.attachToVehicle}} />
<Button @icon="unlink" @text="Detach" @size="xs" @type="danger" @onClick={{this.detachFromVehicle}} />
<Button @icon="shuffle" @text={{t "device.attachment.change"}} @size="xs" @onClick={{this.attachToVehicle}} />
<Button @icon="unlink" @text={{t "device.attachment.detach"}} @size="xs" @type="danger" @onClick={{this.detachFromVehicle}} />
{{else}}
<Button @icon="link" @text="Attach Vehicle" @size="xs" @onClick={{this.attachToVehicle}} />
<Button @icon="link" @text={{t "device.actions.attach-to-asset"}} @size="xs" @onClick={{this.attachToVehicle}} />
{{/if}}
</div>
</div>
Expand All @@ -39,15 +39,15 @@
<div class="mt-1 text-xs text-gray-500 dark:text-gray-300">{{n-a this.vehicleSubtitle}}</div>
<div class="mt-3 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3">
<div>
<div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Driver</div>
<div class="font-semibold uppercase text-gray-400 dark:text-gray-500">{{t "resource.driver"}}</div>
<div class="truncate text-gray-800 dark:text-gray-100">{{n-a this.vehicleDriverName}}</div>
</div>
<div>
<div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Attached Device</div>
<div class="font-semibold uppercase text-gray-400 dark:text-gray-500">{{t "device.attachment.attached-device"}}</div>
<div class="truncate text-gray-800 dark:text-gray-100">{{n-a this.device.displayName this.device.name}}</div>
</div>
<div>
<div class="font-semibold uppercase text-gray-400 dark:text-gray-500">Device Last Seen</div>
<div class="font-semibold uppercase text-gray-400 dark:text-gray-500">{{t "device.attachment.last-seen"}}</div>
<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>
</div>
</div>
Expand All @@ -58,9 +58,9 @@
<Telematic::TabEmptyState
@tone="warning"
@icon="link-slash"
@title="No vehicle attached"
@message="Attach this device to a vehicle so telemetry has fleet context."
@primaryText="Attach Vehicle"
@title={{t "device.attachment.none-title"}}
@message={{t "device.attachment.none-message"}}
@primaryText={{t "device.actions.attach-to-asset"}}
@primaryIcon="link"
@primaryAction={{this.attachToVehicle}}
/>
Expand Down
8 changes: 8 additions & 0 deletions addon/components/device/panel-tabs/vehicle.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default class DevicePanelTabsVehicleComponent extends Component {
@service hostRouter;
@service mapManager;
@service vehicleActions;
@service trailerActions;

get device() {
return this.args.resource ?? this.args.model;
Expand All @@ -16,6 +17,10 @@ export default class DevicePanelTabsVehicleComponent extends Component {
return this.device?.attachable;
}

get isTrailer() {
return `${this.device?.attachable_type ?? ''}`.toLowerCase().includes('trailer');
}

get vehicleName() {
return this.device?.attached_to_name ?? this.vehicle?.displayName ?? this.vehicle?.display_name ?? this.vehicle?.name;
}
Expand Down Expand Up @@ -58,6 +63,9 @@ export default class DevicePanelTabsVehicleComponent extends Component {

@action openVehicle() {
if (this.vehicle?.id) {
if (this.isTrailer) {
return this.trailerActions.transition.view(this.vehicle);
}
return this.vehicleActions.panel?.view
? this.vehicleActions.panel.view(this.vehicle)
: this.hostRouter.transitionTo('console.fleet-ops.management.vehicles.index.details', this.vehicle);
Expand Down
15 changes: 11 additions & 4 deletions addon/components/equipment/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,24 @@ import { task } from 'ember-concurrency';
const TYPE_TO_MODEL = {
'fleet-ops:vehicle': 'vehicle',
'fleet-ops:driver': 'driver',
'fleet-ops:trailer': 'trailer',
'Fleetbase\\FleetOps\\Models\\Vehicle': 'vehicle',
'Fleetbase\\FleetOps\\Models\\Driver': 'driver',
'Fleetbase\\FleetOps\\Models\\Trailer': 'trailer',
'fleet-ops:equipment': 'equipment',
};

const TYPE_TO_OPTION_VALUE = {
'Fleetbase\\FleetOps\\Models\\Vehicle': 'fleet-ops:vehicle',
'Fleetbase\\FleetOps\\Models\\Driver': 'fleet-ops:driver',
'Fleetbase\\FleetOps\\Models\\Trailer': 'fleet-ops:trailer',
};

export default class EquipmentFormComponent extends Component {
@service fetch;
@service currentUser;
@service notifications;
@service intl;

/** Equipment type options. */
equipmentTypeOptions = ['ppe', 'refrigeration_unit', 'tool', 'liftgate', 'ramp', 'container', 'pallet_jack', 'forklift', 'safety_equipment', 'communication_device', 'other'];
Expand All @@ -36,10 +40,13 @@ export default class EquipmentFormComponent extends Component {
* Polymorphic equipable type options — the asset this equipment is attached to.
* Each entry has a `value` (stored on the model) and a `label` (displayed in the UI).
*/
equipableTypeOptions = [
{ value: 'fleet-ops:vehicle', label: 'Vehicle' },
{ value: 'fleet-ops:driver', label: 'Driver' },
];
get equipableTypeOptions() {
return [
{ value: 'fleet-ops:vehicle', label: this.intl.t('resource.vehicle') },
{ value: 'fleet-ops:trailer', label: this.intl.t('resource.trailer') },
{ value: 'fleet-ops:driver', label: this.intl.t('resource.driver') },
];
}

/** Derived Ember Data model name for the currently selected equipable type. */
@tracked equipableModelName = null;
Expand Down
16 changes: 9 additions & 7 deletions addon/components/layout/fleet-ops-sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
]),
this.createItem('menu.drivers', 'id-card', 'management.drivers', 'fleet-ops list driver', 'fleet-ops see driver', ['driver', 'online drivers']),
this.createItem('menu.vehicles', 'truck', 'management.vehicles', 'fleet-ops list vehicle', 'fleet-ops see vehicle', ['vehicle', 'track vehicles', 'online vehicles']),
this.createItem('menu.trailers', 'trailer', 'management.trailers', 'fleet-ops list trailer', 'fleet-ops see trailer', ['trailer', 'towed asset', 'reefer', 'flatbed']),
this.createItem('menu.fleets', 'user-group', 'management.fleets', 'fleet-ops list fleet', 'fleet-ops see fleet', ['fleet', 'teams']),
this.createItem('menu.vendors', 'warehouse', 'management.vendors', 'fleet-ops list vendor', 'fleet-ops see vendor'),
this.createItem('menu.contacts', 'address-book', 'management.contacts', 'fleet-ops list contact', 'fleet-ops see contact'),
Expand Down Expand Up @@ -346,13 +347,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
'management.index': 0,
'management.drivers': 1,
'management.vehicles': 2,
'management.fleets': 3,
'management.vendors': 4,
'management.contacts': 5,
'management.places': 6,
'management.fuel-reports': 7,
'management.fuel-transactions': 8,
'management.issues': 9,
'management.trailers': 3,
'management.fleets': 4,
'management.vendors': 5,
'management.contacts': 6,
'management.places': 7,
'management.fuel-reports': 8,
'management.fuel-transactions': 9,
'management.issues': 10,
'maintenance.index': 0,
'maintenance.schedules': 1,
'maintenance.work-orders': 2,
Expand Down
20 changes: 13 additions & 7 deletions addon/components/maintenance-schedule/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { isArray } from '@ember/array';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

/**
* Maps a polymorphic type string to the Ember Data model name used by ModelSelect.
*/
const TYPE_TO_MODEL = {
'fleet-ops:vehicle': 'vehicle',
'fleet-ops:trailer': 'trailer',
'fleet-ops:equipment': 'equipment',
'fleet-ops:vendor': 'vendor',
'fleet-ops:contact': 'contact',
Expand All @@ -21,9 +23,11 @@ const TYPE_TO_MODEL = {
*/
const MODEL_TO_TYPE = {
'maintenance-subject-vehicle': 'fleet-ops:vehicle',
'maintenance-subject-trailer': 'fleet-ops:trailer',
'maintenance-subject-equipment': 'fleet-ops:equipment',
// Fall-through for raw vehicle/equipment models passed from vehicle-actions.js
vehicle: 'fleet-ops:vehicle',
trailer: 'fleet-ops:trailer',
equipment: 'fleet-ops:equipment',
vendor: 'fleet-ops:vendor',
contact: 'fleet-ops:contact',
Expand All @@ -44,11 +48,6 @@ const ASSIGNEE_MODEL_TO_TYPE = {
driver: 'fleet-ops:driver',
};

const SUBJECT_TYPE_OPTIONS = [
{ label: 'Vehicle', value: 'fleet-ops:vehicle' },
{ label: 'Equipment', value: 'fleet-ops:equipment' },
];

const ASSIGNEE_TYPE_OPTIONS = [
{ label: 'Vendor', value: 'fleet-ops:vendor' },
{ label: 'Contact', value: 'fleet-ops:contact' },
Expand All @@ -62,6 +61,7 @@ const INTERVAL_METHOD_OPTIONS = [
];

export default class MaintenanceScheduleFormComponent extends Component {
@service intl;
@tracked selectedSubjectType = null;
@tracked selectedAssigneeType = null;
@tracked subjectModelName = null;
Expand All @@ -70,7 +70,13 @@ export default class MaintenanceScheduleFormComponent extends Component {
@tracked reminderOffsets = [];
@tracked newReminderOffset = '';

subjectTypeOptions = SUBJECT_TYPE_OPTIONS;
get subjectTypeOptions() {
return [
{ label: this.intl.t('resource.vehicle'), value: 'fleet-ops:vehicle' },
{ label: this.intl.t('resource.trailer'), value: 'fleet-ops:trailer' },
{ label: this.intl.t('resource.equipment'), value: 'fleet-ops:equipment' },
];
}
assigneeTypeOptions = ASSIGNEE_TYPE_OPTIONS;
intervalMethodOptions = INTERVAL_METHOD_OPTIONS;

Expand Down Expand Up @@ -103,7 +109,7 @@ export default class MaintenanceScheduleFormComponent extends Component {
const modelName = subject.constructor?.modelName ?? subject.modelName;
const typeValue = MODEL_TO_TYPE[modelName] ?? null;
if (typeValue) {
this.selectedSubjectType = SUBJECT_TYPE_OPTIONS.find((o) => o.value === typeValue) ?? null;
this.selectedSubjectType = this.subjectTypeOptions.find((o) => o.value === typeValue) ?? null;
this.subjectModelName = TYPE_TO_MODEL[typeValue] ?? null;
}
}
Expand Down
Loading
Loading