diff --git a/.github/workflows/postman.yml b/.github/workflows/postman.yml index 702cd6437..eb3cb8655 100644 --- a/.github/workflows/postman.yml +++ b/.github/workflows/postman.yml @@ -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 diff --git a/addon/components/cell/attached-vehicle.hbs b/addon/components/cell/attached-vehicle.hbs index 6ad15e212..a79290f88 100644 --- a/addon/components/cell/attached-vehicle.hbs +++ b/addon/components/cell/attached-vehicle.hbs @@ -4,7 +4,7 @@ {{else}}
- Unattached + {{t "trailer.attachment.detached"}}
{{/if}} diff --git a/addon/components/cell/attached-vehicle.js b/addon/components/cell/attached-vehicle.js index 42c1645a8..b34175b44 100644 --- a/addon/components/cell/attached-vehicle.js +++ b/addon/components/cell/attached-vehicle.js @@ -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) { diff --git a/addon/components/device/panel-tabs/vehicle.hbs b/addon/components/device/panel-tabs/vehicle.hbs index 292b514a8..b591cdf5d 100644 --- a/addon/components/device/panel-tabs/vehicle.hbs +++ b/addon/components/device/panel-tabs/vehicle.hbs @@ -1,21 +1,21 @@
-

Vehicle Attachment

-

Fleet context for telemetry from this device.

+

{{t "device.attachment.title"}}

+

{{t "device.attachment.description"}}

{{#if this.hasVehicle}} {{#if this.canOpenVehicle}} -
@@ -39,15 +39,15 @@
{{n-a this.vehicleSubtitle}}
-
Driver
+
{{t "resource.driver"}}
{{n-a this.vehicleDriverName}}
-
Attached Device
+
{{t "device.attachment.attached-device"}}
{{n-a this.device.displayName this.device.name}}
-
Device Last Seen
+
{{t "device.attachment.last-seen"}}
{{n-a (format-date-fns this.device.last_online_at "dd MMM yyyy, HH:mm")}}
@@ -58,9 +58,9 @@ diff --git a/addon/components/device/panel-tabs/vehicle.js b/addon/components/device/panel-tabs/vehicle.js index 49c1e8d28..05862f2bc 100644 --- a/addon/components/device/panel-tabs/vehicle.js +++ b/addon/components/device/panel-tabs/vehicle.js @@ -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; @@ -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; } @@ -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); diff --git a/addon/components/equipment/form.js b/addon/components/equipment/form.js index cb855a906..d7b369569 100644 --- a/addon/components/equipment/form.js +++ b/addon/components/equipment/form.js @@ -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']; @@ -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; diff --git a/addon/components/layout/fleet-ops-sidebar.js b/addon/components/layout/fleet-ops-sidebar.js index f045dd88f..feaee9553 100644 --- a/addon/components/layout/fleet-ops-sidebar.js +++ b/addon/components/layout/fleet-ops-sidebar.js @@ -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'), diff --git a/addon/components/maintenance-schedule/form.js b/addon/components/maintenance-schedule/form.js index 7b957b306..a9aeca8a2 100644 --- a/addon/components/maintenance-schedule/form.js +++ b/addon/components/maintenance-schedule/form.js @@ -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', @@ -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', @@ -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' }, @@ -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; @@ -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; @@ -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; } } diff --git a/addon/components/maintenance/form.js b/addon/components/maintenance/form.js index cd633049e..d5339843c 100644 --- a/addon/components/maintenance/form.js +++ b/addon/components/maintenance/form.js @@ -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', @@ -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', }; @@ -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']; @@ -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. diff --git a/addon/components/modals/attach-equipment.hbs b/addon/components/modals/attach-equipment.hbs new file mode 100644 index 000000000..45d8e87af --- /dev/null +++ b/addon/components/modals/attach-equipment.hbs @@ -0,0 +1,9 @@ + + + diff --git a/addon/components/modals/attach-equipment.js b/addon/components/modals/attach-equipment.js new file mode 100644 index 000000000..197f863c3 --- /dev/null +++ b/addon/components/modals/attach-equipment.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class ModalsAttachEquipmentComponent extends Component {} diff --git a/addon/components/modals/attach-telematic-device.hbs b/addon/components/modals/attach-telematic-device.hbs index 933de41a2..0ae518173 100644 --- a/addon/components/modals/attach-telematic-device.hbs +++ b/addon/components/modals/attach-telematic-device.hbs @@ -1,25 +1,30 @@ diff --git a/addon/components/modals/attach-telematic-device.js b/addon/components/modals/attach-telematic-device.js index a975b9960..21daea432 100644 --- a/addon/components/modals/attach-telematic-device.js +++ b/addon/components/modals/attach-telematic-device.js @@ -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') }, + ]; + } +} diff --git a/addon/components/modals/attach-trailer.hbs b/addon/components/modals/attach-trailer.hbs new file mode 100644 index 000000000..c7aa94ea2 --- /dev/null +++ b/addon/components/modals/attach-trailer.hbs @@ -0,0 +1,18 @@ + + + diff --git a/addon/components/modals/attach-trailer.js b/addon/components/modals/attach-trailer.js new file mode 100644 index 000000000..94a5bfdba --- /dev/null +++ b/addon/components/modals/attach-trailer.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ember-core/components/modal/base'; diff --git a/addon/components/modals/select-trailer.hbs b/addon/components/modals/select-trailer.hbs new file mode 100644 index 000000000..69d184f76 --- /dev/null +++ b/addon/components/modals/select-trailer.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/modals/select-trailer.js b/addon/components/modals/select-trailer.js new file mode 100644 index 000000000..94a5bfdba --- /dev/null +++ b/addon/components/modals/select-trailer.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ember-core/components/modal/base'; diff --git a/addon/components/trailer/card.hbs b/addon/components/trailer/card.hbs new file mode 100644 index 000000000..402727e96 --- /dev/null +++ b/addon/components/trailer/card.hbs @@ -0,0 +1,5 @@ + +
{{or @resource.name @resource.yearMakeModel}}
{{or @resource.plate_number @resource.vin @resource.public_id}}
+
{{@resource.status}}
+
{{t "trailer.fields.updated"}}: {{@resource.updatedAt}}
+
diff --git a/addon/components/trailer/card.js b/addon/components/trailer/card.js new file mode 100644 index 000000000..d9776060c --- /dev/null +++ b/addon/components/trailer/card.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +export default class TrailerCardComponent extends Component { + @service trailerActions; +} diff --git a/addon/components/trailer/connections.hbs b/addon/components/trailer/connections.hbs new file mode 100644 index 000000000..c8c10a881 --- /dev/null +++ b/addon/components/trailer/connections.hbs @@ -0,0 +1 @@ +

{{t "trailer.tabs.connections"}}

{{#if @resource.connections.length}}{{#each @resource.connections as |connection|}}{{/each}}
{{t "trailer.fields.vehicle"}}{{t "trailer.fields.connected-at"}}{{t "trailer.fields.disconnected-at"}}{{t "common.status"}}
{{n-a connection.vehicle.name}}{{n-a connection.connected_at}}{{n-a connection.disconnected_at}}
{{else}}
{{t "trailer.empty.connections"}}
{{/if}}
diff --git a/addon/components/trailer/connections.js b/addon/components/trailer/connections.js new file mode 100644 index 000000000..b1d2fe501 --- /dev/null +++ b/addon/components/trailer/connections.js @@ -0,0 +1,2 @@ +import Component from '@glimmer/component'; +export default class TrailerConnectionsComponent extends Component {} diff --git a/addon/components/trailer/details.hbs b/addon/components/trailer/details.hbs new file mode 100644 index 000000000..a2db1abbf --- /dev/null +++ b/addon/components/trailer/details.hbs @@ -0,0 +1,8 @@ +
+
{{t "trailer.fields.type"}}
{{n-a @resource.type}}
{{t "column.plate-number"}}
{{n-a @resource.plate_number}}
{{t "trailer.fields.vin"}}
{{n-a @resource.vin}}
{{t "trailer.fields.serial-number"}}
{{n-a @resource.serial_number}}
{{t "column.make"}}
{{n-a @resource.make}}
{{t "column.model"}}
{{n-a @resource.model}}
{{t "column.year"}}
{{n-a @resource.year}}
{{t "common.status"}}
+
{{t "trailer.fields.vehicle"}}
{{n-a @resource.current_vehicle.name}}
{{t "trailer.fields.attachment-state"}}
+
{{t "trailer.fields.connectivity"}}
{{t "trailer.fields.last-online"}}
{{n-a @resource.last_online_at}}
{{t "common.location"}}
{{n-a @resource.current_location}}
{{t "trailer.fields.devices-count"}}
{{or @resource.devices_count 0}}
+
{{t "trailer.fields.length"}}
{{n-a @resource.length}}
{{t "trailer.fields.gvwr"}}
{{n-a @resource.gvwr}}
{{t "trailer.fields.payload"}}
{{n-a @resource.payload_capacity}}
{{t "trailer.fields.axles"}}
{{n-a @resource.axle_count}}
+ + +
diff --git a/addon/components/trailer/details.js b/addon/components/trailer/details.js new file mode 100644 index 000000000..9c8b13021 --- /dev/null +++ b/addon/components/trailer/details.js @@ -0,0 +1,2 @@ +import Component from '@glimmer/component'; +export default class TrailerDetailsComponent extends Component {} diff --git a/addon/components/trailer/equipment.hbs b/addon/components/trailer/equipment.hbs new file mode 100644 index 000000000..42a026698 --- /dev/null +++ b/addon/components/trailer/equipment.hbs @@ -0,0 +1 @@ +
{{#if this.load.isRunning}}{{else if this.equipment.length}}
{{#each this.equipment as |item|}}
{{item.name}}
{{or item.type item.serial_number}}
{{/each}}
{{else}}
{{t "trailer.empty.equipment"}}
{{/if}}
diff --git a/addon/components/trailer/equipment.js b/addon/components/trailer/equipment.js new file mode 100644 index 000000000..26e9a1aeb --- /dev/null +++ b/addon/components/trailer/equipment.js @@ -0,0 +1,31 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +export default class TrailerEquipmentComponent extends Component { + @service store; + @service notifications; + @service equipmentActions; + @service trailerActions; + @tracked equipment = []; + constructor() { + super(...arguments); + this.load.perform(); + } + @task *load() { + try { + this.equipment = yield this.store.query('equipment', { + equipable_type: 'fleet-ops:trailer', + equipable: this.args.resource.public_id ?? this.args.resource.id, + sort: '-created_at', + }); + } catch (error) { + this.notifications.serverError(error); + } + } + attach = () => this.trailerActions.attachEquipment(this.args.resource); + detach = async (equipment) => { + await this.trailerActions.detachEquipment(this.args.resource, equipment); + this.load.perform(); + }; +} diff --git a/addon/components/trailer/form.hbs b/addon/components/trailer/form.hbs new file mode 100644 index 000000000..348def397 --- /dev/null +++ b/addon/components/trailer/form.hbs @@ -0,0 +1,43 @@ +
+ + +
{{@resource.name}}
+
+ + +