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
47 changes: 47 additions & 0 deletions addon/models/asset-connection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Model, { attr, belongsTo } from '@ember-data/model';
import { computed } from '@ember/object';
import { format as formatDate, isValid as isValidDate, formatDistanceStrict } from 'date-fns';

export default class AssetConnectionModel extends Model {
@attr('string') uuid;
@attr('string') public_id;
@attr('string') company_uuid;
@attr('string') connector_type;
@attr('string') connector_uuid;
@attr('string') connected_type;
@attr('string') connected_uuid;
@attr('string', { defaultValue: 'towing' }) relationship_type;
@attr('number', { defaultValue: 1 }) position;
@attr('string') source;
@attr('string') confidence;
@attr('string') notes;
@attr('raw') meta;
@attr('boolean') active;
@attr('date') connected_at;
@attr('date') disconnected_at;
@attr('date') created_at;
@attr('date') updated_at;

@belongsTo('vehicle', { async: false, inverse: null }) vehicle;
@belongsTo('trailer', { async: false, inverse: null }) trailer;

@computed('active', 'disconnected_at') get isActive() {
return this.active === true || (this.active !== false && !this.disconnected_at);
}

@computed('connected_at') get connectedAt() {
return isValidDate(this.connected_at) ? formatDate(this.connected_at, 'yyyy-MM-dd HH:mm') : null;
}

@computed('disconnected_at') get disconnectedAt() {
return isValidDate(this.disconnected_at) ? formatDate(this.disconnected_at, 'yyyy-MM-dd HH:mm') : null;
}

@computed('connected_at', 'disconnected_at') get duration() {
if (!isValidDate(this.connected_at)) {
return null;
}

return formatDistanceStrict(this.connected_at, isValidDate(this.disconnected_at) ? this.disconnected_at : new Date());
}
}
16 changes: 15 additions & 1 deletion addon/models/asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,22 @@ export default class AssetModel extends Model {
@attr('string') odometer_unit;
@attr('string') transmission;
@attr('string') fuel_volume_unit;
@attr('string') fuel_Type;
@attr('string') fuel_type;
@attr('string') ownership_type;
@attr('string') engine_hours;
@attr('string') gvw;
@attr('number') width;
@attr('number') length;
@attr('number') height;
@attr('number') tare_weight;
@attr('number') gvwr;
@attr('number') payload_capacity;
@attr('number') cargo_volume;
@attr('string') currency;
@attr('string') acquisition_cost;
@attr('string') current_value;
@attr('string') insurance_value;
@attr('string') depreciation_rate;
@attr('raw') capacity;
@attr('raw') specs;
@attr('raw') attributes;
Expand All @@ -77,6 +89,8 @@ export default class AssetModel extends Model {

/** @dates */
@attr('date') deleted_at;
@attr('date') purchased_at;
@attr('date') lease_expires_at;
@attr('date') created_at;
@attr('date') updated_at;

Expand Down
18 changes: 18 additions & 0 deletions addon/models/attachable-driver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import AttachableModel from './attachable';
import { attr } from '@ember-data/model';

/**
* Concrete polymorphic model for a Driver that equipment is issued to.
*
* Drivers are not telematics attachables, but equipment can be equipped to a driver
* (`fleet-ops:driver`), and the equipment serializer resolves that polymorphic
* relationship through the attachable model family.
*/
export default class AttachableDriverModel extends AttachableModel {
@attr('string') internal_id;
@attr('string') phone;
@attr('string') email;
@attr('string') drivers_license_number;
@attr('string') vehicle_name;
@attr('string') vendor_name;
}
11 changes: 11 additions & 0 deletions addon/models/attachable-trailer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import AttachableAssetModel from './attachable-asset';
import { attr } from '@ember-data/model';

/** Concrete polymorphic model for a Trailer attached to a device. */
export default class AttachableTrailerModel extends AttachableAssetModel {
@attr('string') body_type;
@attr('string') coupling_type;
@attr('number') axle_count;
@attr('boolean') refrigerated;
@attr('string') current_vehicle_name;
}
2 changes: 1 addition & 1 deletion addon/models/attachable.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro

/**
* Abstract base model for resources a device can be attached to.
* Concrete types: attachable-vehicle, attachable-asset.
* Concrete types: attachable-vehicle, attachable-asset, attachable-trailer.
*/
export default class AttachableModel extends Model {
/** @ids */
Expand Down
1 change: 1 addition & 0 deletions addon/models/equipment.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default class EquipmentModel extends Model {
/** @relationships */
@belongsTo('warranty', { async: false }) warranty;
@belongsTo('file', { async: false }) photo;
@belongsTo('attachable', { polymorphic: true, async: false }) equipable;
@hasMany('maintenance', { async: false }) maintenances;
@hasMany('custom-field-value', { async: false }) custom_field_values;

Expand Down
15 changes: 15 additions & 0 deletions addon/models/maintenance-subject-trailer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import MaintenanceSubjectModel from './maintenance-subject';
import { attr } from '@ember-data/model';

/** Concrete polymorphic model for Trailer maintenance targets. */
export default class MaintenanceSubjectTrailerModel extends MaintenanceSubjectModel {
@attr('string') code;
@attr('string') vin;
@attr('string') plate_number;
@attr('string') make;
@attr('string') model;
@attr('string') year;
@attr('string') body_type;
@attr('number') axle_count;
@attr('string') current_vehicle_name;
}
4 changes: 3 additions & 1 deletion addon/models/maintenance-subject.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro

/**
* Abstract base model for polymorphic maintenance subjects.
* Concrete types: maintenance-subject-vehicle, maintenance-subject-equipment
* Concrete types: maintenance-subject-vehicle, maintenance-subject-trailer,
* maintenance-subject-equipment
*
* The backend stores the type as a PolymorphicType cast string, e.g.:
* 'fleet-ops:vehicle' -> maintenance-subject-vehicle
* 'fleet-ops:trailer' -> maintenance-subject-trailer
* 'fleet-ops:equipment' -> maintenance-subject-equipment
*/
export default class MaintenanceSubjectModel extends Model {
Expand Down
129 changes: 129 additions & 0 deletions addon/models/trailer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import AssetModel from './asset';
import { attr, belongsTo, hasMany } from '@ember-data/model';
import { computed, get } from '@ember/object';
import { not } from '@ember/object/computed';
import isValidCoordinates from '@fleetbase/ember-core/utils/is-valid-coordinates';
import { format as formatDate, isValid as isValidDate, formatDistanceToNow } from 'date-fns';

/**
* A first-class towed fleet asset.
*
* Trailer records share the common Asset contract while exposing the
* operational, connection, capacity, and telemetry fields used by Fleet-Ops.
*/
export default class TrailerModel extends AssetModel {
/** @relationships */
@belongsTo('vehicle', { async: false, inverse: null }) current_vehicle;
@belongsTo('asset-connection', { async: false, inverse: null }) current_connection;
@hasMany('asset-connection', { async: false, inverse: null }) connections;
@hasMany('maintenance-schedule', { async: false, inverse: null }) maintenance_schedules;
@hasMany('work-order', { async: false, inverse: null }) work_orders;
@hasMany('position', { async: false, inverse: null }) positions;

/** @classification */
@attr('string', { defaultValue: 'trailer' }) asset_class;
@attr('string') body_type;
@attr('string') coupling_type;
@attr('string') brake_type;

/** @capacity and dimensions */
@attr('number') length;
@attr('number') width;
@attr('number') height;
@attr('number') tare_weight;
@attr('number') gvwr;
@attr('number') payload_capacity;
@attr('number') cargo_volume;
@attr('number') axle_count;
@attr('number') tire_count;
@attr('number') door_count;

/** @specialized trailer capabilities */
@attr('boolean') abs_equipped;
@attr('boolean') ebs_equipped;
@attr('boolean') refrigerated;
@attr('number') temperature_min;
@attr('number') temperature_max;
@attr('number') reefer_engine_hours;

/** @current operational projections */
@attr('boolean') online;
@attr('string') attachment_state;
@attr('string') vehicle_id;
@attr('string') connectivity_status;
@attr('string') movement_status;
@attr('raw') telematics;
@attr('raw') resolved_location;
@attr('string') current_vehicle_name;
@attr('string') current_vehicle_id;
@attr('date') attached_at;
@attr('number') devices_count;
@attr('number') equipment_count;
@attr('date') last_online_at;

@computed('display_name', 'name', 'yearMakeModel', 'code', 'public_id') get displayName() {
return this.display_name || this.name || this.yearMakeModel || this.code || this.public_id;
}

@computed('attachment_state') get isAttached() {
return this.attachment_state === 'attached';
}

@computed('connectivity_status', 'online') get isOnline() {
return this.connectivity_status === 'online' || this.online === true;
}

@computed('last_online_at') get lastOnlineAt() {
if (!isValidDate(this.last_online_at)) {
return null;
}

return formatDate(this.last_online_at, 'yyyy-MM-dd HH:mm');
}

@computed('last_online_at') get lastOnlineAgo() {
if (!isValidDate(this.last_online_at)) {
return null;
}

return formatDistanceToNow(this.last_online_at, { addSuffix: true });
}

@computed('attached_at') get attachedAt() {
if (!isValidDate(this.attached_at)) {
return null;
}

return formatDate(this.attached_at, 'yyyy-MM-dd HH:mm');
}

@computed('name', 'display_name', 'code', 'plate_number', 'vin', 'serial_number', 'yearMakeModel') get searchString() {
return [this.name, this.display_name, this.code, this.plate_number, this.vin, this.serial_number, this.yearMakeModel].filter(Boolean).join(' ');
}

@computed('location') get longitude() {
return get(this.location, 'coordinates.0');
}

@computed('location') get latitude() {
return get(this.location, 'coordinates.1');
}

@computed('latitude', 'longitude') get coordinates() {
return [get(this, 'latitude'), get(this, 'longitude')];
}

@computed('latitude', 'longitude') get latlng() {
return { lat: get(this, 'latitude'), lng: get(this, 'longitude') };
}

@computed('coordinates', 'latitude', 'longitude') get hasValidCoordinates() {
if (this.longitude === 0 || this.latitude === 0) {
return false;
}

return isValidCoordinates(this.coordinates);
}

@not('hasValidCoordinates') hasInvalidCoordinates;
}
3 changes: 3 additions & 0 deletions addon/models/vehicle.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export default class VehicleModel extends Model {
@belongsTo('driver', { async: false }) driver;
@belongsTo('vendor', { async: false }) vendor;
@hasMany('device', { async: false }) devices;
@hasMany('trailer', { async: false, inverse: null }) trailers;
@hasMany('asset-connection', { async: false, inverse: null }) trailer_connections;
@hasMany('equipment', { async: false, inverse: null }) equipments;
@hasMany('custom-field-value', { async: false }) custom_field_values;

/** @attributes */
Expand Down
11 changes: 11 additions & 0 deletions addon/serializers/asset-connection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';

export default class AssetConnectionSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
get attrs() {
return {
vehicle: { embedded: 'always' },
trailer: { embedded: 'always' },
};
}
}
2 changes: 1 addition & 1 deletion addon/serializers/device.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default class DeviceSerializer extends ApplicationSerializer.extend(Embed
.replace(/^attachable-/, '')
.toLowerCase();

if (!['vehicle', 'asset'].includes(type)) {
if (!['vehicle', 'asset', 'trailer'].includes(type)) {
return undefined;
}

Expand Down
65 changes: 64 additions & 1 deletion addon/serializers/equipment.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,67 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
import { isBlank } from '@ember/utils';

export default class EquipmentSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {}
export default class EquipmentSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
get attrs() {
return {
warranty: { embedded: 'always' },
photo: { embedded: 'always' },
equipable: { embedded: 'always' },
custom_field_values: { embedded: 'always' },
};
}

normalize(model, hash, prop) {
const equipableDomainType = hash?.equipable?.type;

if (hash?.equipable) {
hash.equipable.type = this.equipableModelNameFromType(hash.equipable_type);
}

const normalized = super.normalize(model, hash, prop);

if (equipableDomainType && !this.equipableModelNameFromType(equipableDomainType)) {
const equipable = normalized?.data?.relationships?.equipable?.data;
const included = normalized?.included?.find((resource) => resource.type === equipable?.type && resource.id === equipable?.id);

if (included) {
included.attributes = included.attributes ?? {};
included.attributes.type = equipableDomainType;
}
}

return normalized;
}

serializePolymorphicType(snapshot, json, relationship) {
let key = relationship.key;

if (key !== 'equipable') {
return typeof super.serializePolymorphicType === 'function' ? super.serializePolymorphicType(...arguments) : undefined;
}

const belongsTo = snapshot.belongsTo(key);

if (!isBlank(snapshot.attr(`${key}_type`))) {
return;
}

key = this.keyForAttribute ? this.keyForAttribute(key, 'serialize') : key;
json[`${key}_type`] = belongsTo ? `fleet-ops:${belongsTo.modelName.replace(/^attachable-/, '')}` : null;
}

equipableModelNameFromType(type) {
if (!type || typeof type !== 'string') {
return undefined;
}

const normalized = type
.split('\\')
.pop()
.replace(/^fleet-ops:/, '')
.replace(/^attachable-/, '')
.toLowerCase();
return ['vehicle', 'trailer', 'driver', 'asset'].includes(normalized) ? `attachable-${normalized}` : undefined;
}
}
Loading
Loading