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' && 'e1752902891eb625a53d8f3705bec496610641f1' || '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
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
1 change: 1 addition & 0 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
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
16 changes: 12 additions & 4 deletions addon/components/maintenance/form.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

/**
* Maps a polymorphic type value to the Ember Data model name used by ModelSelect.
*/
const TYPE_TO_MODEL = {
'fleet-ops:vehicle': 'vehicle',
'fleet-ops:trailer': 'trailer',
'fleet-ops:driver': 'driver',
'fleet-ops:equipment': 'equipment',
'fleet-ops:vendor': 'vendor',
Expand All @@ -20,9 +22,11 @@ const TYPE_TO_MODEL = {
*/
const MAINTAINABLE_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 models passed from vehicle-actions.js
vehicle: 'fleet-ops:vehicle',
trailer: 'fleet-ops:trailer',
equipment: 'fleet-ops:equipment',
};

Expand All @@ -40,6 +44,7 @@ const PERFORMED_BY_MODEL_TO_TYPE = {
};

export default class MaintenanceFormComponent extends Component {
@service intl;
/** Maintenance type options — the category of maintenance activity. */
maintenanceTypeOptions = ['preventive', 'corrective', 'predictive', 'routine', 'emergency', 'inspection', 'repair', 'replacement', 'calibration'];

Expand All @@ -54,10 +59,13 @@ export default class MaintenanceFormComponent extends Component {
* Uses label/value objects so the PowerSelect trigger shows a human-readable
* label instead of the raw model type string.
*/
maintainableTypeOptions = [
{ value: 'fleet-ops:vehicle', label: 'Vehicle' },
{ value: 'fleet-ops:equipment', label: 'Equipment' },
];
get maintainableTypeOptions() {
return [
{ value: 'fleet-ops:vehicle', label: this.intl.t('resource.vehicle') },
{ value: 'fleet-ops:trailer', label: this.intl.t('resource.trailer') },
{ value: 'fleet-ops:equipment', label: this.intl.t('resource.equipment') },
];
}

/**
* Polymorphic performed-by type options — who carried out the maintenance.
Expand Down
9 changes: 9 additions & 0 deletions addon/components/modals/attach-equipment.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container">
<InputGroup @name={{t "common.select-field" field=(t "resource.equipment")}} @value={{@options.selectedEquipment}}>
<ModelSelect @modelName="equipment" @selectedModel={{@options.selectedEquipment}} @query={{hash attachment_state="detached"}} @placeholder={{t "trailer.messages.select-equipment"}} @triggerClass="form-select form-input" @infiniteScroll={{true}} @renderInPlace={{true}} @onChange={{fn (mut @options.selectedEquipment)}} as |equipment|>
{{or equipment.name equipment.public_id}}
</ModelSelect>
</InputGroup>
</div>
</Modal::Default>
3 changes: 3 additions & 0 deletions addon/components/modals/attach-equipment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Component from '@glimmer/component';

export default class ModalsAttachEquipmentComponent extends Component {}
21 changes: 13 additions & 8 deletions addon/components/modals/attach-telematic-device.hbs
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container">
<div class="mb-4 rounded-md border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Synced device</div>
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">{{t "device.attachment.synced-device"}}</div>
<div class="mt-2">
<Device::Pill @device={{@options.device}} />
</div>
</div>

<InputGroup @name={{t "common.select-field" field=(t "resource.vehicle")}} @value={{@options.selectedVehicle}}>
<InputGroup @name={{t "device.attachment.asset-type"}}>
<PowerSelect @options={{this.assetTypeOptions}} @selected={{@options.selectedAssetType}} @placeholder={{t "device.attachment.select-asset-type"}} @triggerClass="form-select form-input" @onChange={{fn (mut @options.selectedAssetType)}} as |option|>{{option.label}}</PowerSelect>
</InputGroup>
{{#if @options.selectedAssetType}}
<InputGroup @name={{t "common.select-field" field=@options.selectedAssetType.label}} @value={{@options.selectedAsset}}>
<ModelSelect
@modelName="vehicle"
@selectedModel={{@options.selectedVehicle}}
@placeholder={{t "common.select-field" field=(t "resource.vehicle")}}
@modelName={{@options.selectedAssetType.model}}
@selectedModel={{@options.selectedAsset}}
@placeholder={{t "common.select-field" field=@options.selectedAssetType.label}}
@triggerClass="form-select form-input"
@infiniteScroll={{true}}
@renderInPlace={{true}}
@onChange={{fn (mut @options.selectedVehicle)}}
as |vehicle|
@onChange={{fn (mut @options.selectedAsset)}}
as |asset|
>
<Vehicle::Pill @resource={{vehicle}} />
{{or asset.displayName asset.display_name asset.name asset.public_id}}
</ModelSelect>
</InputGroup>
{{/if}}
</div>
</Modal::Default>
12 changes: 11 additions & 1 deletion addon/components/modals/attach-telematic-device.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class ModalsAttachTelematicDeviceComponent extends Component {}
export default class ModalsAttachTelematicDeviceComponent extends Component {
@service intl;

get assetTypeOptions() {
return [
{ value: 'fleet-ops:vehicle', model: 'vehicle', label: this.intl.t('resource.vehicle') },
{ value: 'fleet-ops:trailer', model: 'trailer', label: this.intl.t('resource.trailer') },
];
}
}
18 changes: 18 additions & 0 deletions addon/components/modals/attach-trailer.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container">
<InputGroup @name={{t "trailer.fields.vehicle"}}>
<ModelSelect
@modelName="vehicle"
@selectedModel={{@options.selectedVehicle}}
@placeholder={{t "trailer.messages.select-vehicle"}}
@triggerClass="form-select form-input"
@infiniteScroll={{true}}
@renderInPlace={{true}}
@onChange={{fn (mut @options.selectedVehicle)}}
as |vehicle|
>
<Vehicle::Pill @vehicle={{vehicle}} />
</ModelSelect>
</InputGroup>
</div>
</Modal::Default>
1 change: 1 addition & 0 deletions addon/components/modals/attach-trailer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/ember-core/components/modal/base';
Loading
Loading