diff --git a/addon/models/asset.js b/addon/models/asset.js index 6596ce0..8387795 100644 --- a/addon/models/asset.js +++ b/addon/models/asset.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class AssetModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') category_uuid; @@ -63,11 +64,16 @@ export default class AssetModel extends Model { @attr('raw') attributes; @attr('string') notes; @attr('string') slug; + /** @server-computed (read-only appended attributes) */ @attr('string') photo_url; @attr('string') display_name; @attr('string') category_name; @attr('string') vendor_name; @attr('string') warranty_name; + @attr('string') current_location; + @attr('boolean') is_online; + @attr('date') last_maintenance; + @attr('date') next_maintenance_due; /** @dates */ @attr('date') deleted_at; diff --git a/addon/models/device.js b/addon/models/device.js index 2c9d4c5..7b93af3 100644 --- a/addon/models/device.js +++ b/addon/models/device.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class DeviceModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') telematic_uuid; @@ -30,7 +31,15 @@ export default class DeviceModel extends Model { @attr('string') imsi; @attr('string') firmware_version; @attr('string') provider; + + /** @server-computed (read-only appended attributes) */ @attr('string') photo_url; + @attr('string') warranty_name; + @attr('string') telematic_name; + @attr('boolean') is_online; + @attr('string') attached_to_name; + @attr('string') connection_status; + @attr('string') manufacturer; @attr('string') serial_number; @attr('point') last_position; diff --git a/addon/models/driver.js b/addon/models/driver.js index a7b17aa..5acb245 100644 --- a/addon/models/driver.js +++ b/addon/models/driver.js @@ -31,6 +31,11 @@ export default class DriverModel extends Model { @belongsTo('vendor', { async: true }) vendor; @hasMany('custom-field-value', { async: false }) custom_field_values; + /** @scheduling-relationships */ + @hasMany('schedule', { async: true }) schedules; + @hasMany('schedule-item', { async: true }) schedule_items; + @hasMany('schedule-availability', { async: true }) availabilities; + /** @attributes */ @attr('string') name; @attr('string') phone; diff --git a/addon/models/equipment.js b/addon/models/equipment.js index bd0e49c..a897ade 100644 --- a/addon/models/equipment.js +++ b/addon/models/equipment.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class EquipmentModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') warranty_uuid; @@ -25,11 +26,17 @@ export default class EquipmentModel extends Model { @attr('string') serial_number; @attr('string') manufacturer; @attr('string') model; - @attr('number') purchase_price; + @attr('string') purchase_price; + @attr('string') currency; @attr('raw') meta; @attr('string') slug; + /** @server-computed (read-only appended attributes) */ @attr('string') warranty_name; @attr('string') photo_url; + @attr('string') equipped_to_name; + @attr('boolean') is_equipped; + @attr('number') age_in_days; + @attr('string') depreciated_value; /** @dates */ @attr('date') purchased_at; diff --git a/addon/models/maintenance-schedule.js b/addon/models/maintenance-schedule.js new file mode 100644 index 0000000..32238b1 --- /dev/null +++ b/addon/models/maintenance-schedule.js @@ -0,0 +1,130 @@ +import Model, { attr, belongsTo } from '@ember-data/model'; +import { computed } from '@ember/object'; +import { format as formatDate, isValid as isValidDate, formatDistanceToNow } from 'date-fns'; + +export default class MaintenanceScheduleModel extends Model { + /** @ids */ + @attr('string') uuid; + @attr('string') public_id; + @attr('string') company_uuid; + + /** @polymorphic relationships */ + @belongsTo('maintenance-subject', { polymorphic: true, async: false }) subject; + @belongsTo('facilitator', { polymorphic: true, async: false }) default_assignee; + /** @computed names — server-side convenience fields (read-only) */ + @attr('string') subject_name; + @attr('string') default_assignee_name; + + /** @attributes */ + @attr('string') code; + @attr('string') title; + @attr('string') description; + @attr('string') name; + @attr('string') type; + @attr('string') status; + @attr('string') interval_method; + + /** @interval — time-based */ + @attr('string') interval_type; + @attr('number') interval_value; + @attr('string') interval_unit; + + /** @interval — distance / engine-hours */ + @attr('number') interval_distance; + @attr('number') interval_engine_hours; + + /** @baseline readings */ + @attr('number') last_service_odometer; + @attr('number') last_service_engine_hours; + @attr('date') last_service_date; + + /** @next-due thresholds */ + @attr('date') next_due_date; + @attr('number') next_due_odometer; + @attr('number') next_due_engine_hours; + + /** @work-order defaults */ + @attr('string') default_priority; + + @attr('string') instructions; + @attr('raw') meta; + @attr('string') slug; + + /** @dates */ + @attr('date') last_triggered_at; + @attr('date') deleted_at; + @attr('date') created_at; + @attr('date') updated_at; + + /** @computed */ + @computed('status') get isActive() { + return this.status === 'active'; + } + + @computed('status') get isPaused() { + return this.status === 'paused'; + } + + @computed('next_due_date') get nextDueAt() { + if (!isValidDate(this.next_due_date)) { + return null; + } + return formatDate(this.next_due_date, 'yyyy-MM-dd HH:mm'); + } + + @computed('next_due_date') get nextDueAtShort() { + if (!isValidDate(this.next_due_date)) { + return null; + } + return formatDate(this.next_due_date, 'dd, MMM yyyy'); + } + + @computed('next_due_date') get nextDueAgo() { + if (!isValidDate(this.next_due_date)) { + return null; + } + return formatDistanceToNow(this.next_due_date, { addSuffix: true }); + } + + @computed('updated_at') get updatedAgo() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDistanceToNow(this.updated_at); + } + + @computed('updated_at') get updatedAt() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDate(this.updated_at, 'yyyy-MM-dd HH:mm'); + } + + @computed('updated_at') get updatedAtShort() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDate(this.updated_at, 'dd, MMM'); + } + + @computed('created_at') get createdAgo() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDistanceToNow(this.created_at); + } + + @computed('created_at') get createdAt() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDate(this.created_at, 'yyyy-MM-dd HH:mm'); + } + + @computed('created_at') get createdAtShort() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDate(this.created_at, 'dd, MMM'); + } +} diff --git a/addon/models/maintenance-subject-equipment.js b/addon/models/maintenance-subject-equipment.js new file mode 100644 index 0000000..d036a1e --- /dev/null +++ b/addon/models/maintenance-subject-equipment.js @@ -0,0 +1,26 @@ +import MaintenanceSubjectModel from './maintenance-subject'; +import { attr } from '@ember-data/model'; + +/** + * Concrete polymorphic model for Equipment acting as a maintenance subject. + * Resolved when the backend sends subject_type / target_type / maintainable_type = 'fleet-ops:equipment'. + */ +export default class MaintenanceSubjectEquipmentModel extends MaintenanceSubjectModel { + /** @ids */ + @attr('string') warranty_uuid; + @attr('string') photo_uuid; + @attr('string') equipable_type; + @attr('string') equipable_uuid; + + /** @attributes */ + @attr('string') code; + @attr('string') serial_number; + @attr('string') manufacturer; + @attr('string') model; + @attr('number') purchase_price; + @attr('string') warranty_name; + @attr('raw') meta; + + /** @dates */ + @attr('date') purchased_at; +} diff --git a/addon/models/maintenance-subject-vehicle.js b/addon/models/maintenance-subject-vehicle.js new file mode 100644 index 0000000..088526b --- /dev/null +++ b/addon/models/maintenance-subject-vehicle.js @@ -0,0 +1,44 @@ +import MaintenanceSubjectModel from './maintenance-subject'; +import { attr } from '@ember-data/model'; +import { get } from '@ember/object'; +import config from 'ember-get-config'; + +/** + * Concrete polymorphic model for a Vehicle acting as a maintenance subject. + * Resolved when the backend sends subject_type / target_type / maintainable_type = 'fleet-ops:vehicle'. + */ +export default class MaintenanceSubjectVehicleModel extends MaintenanceSubjectModel { + /** @ids */ + @attr('string') internal_id; + @attr('string') photo_uuid; + @attr('string') vendor_uuid; + @attr('string') category_uuid; + @attr('string') warranty_uuid; + @attr('string') telematic_uuid; + + /** @attributes */ + @attr('string', { + defaultValue: get(config, 'defaultValues.vehicleImage'), + }) + photo_url; + + @attr('string') make; + @attr('string') model; + @attr('string') year; + @attr('string') trim; + @attr('string') plate_number; + @attr('string') vin; + @attr('string') driver_name; + @attr('string') vendor_name; + @attr('string') display_name; + @attr('string', { + defaultValue: get(config, 'defaultValues.vehicleAvatar'), + }) + avatar_url; + @attr('string') avatar_value; + @attr('string') color; + @attr('string') country; + @attr('number') odometer; + @attr('number') engine_hours; + @attr('raw') meta; +} diff --git a/addon/models/maintenance-subject.js b/addon/models/maintenance-subject.js new file mode 100644 index 0000000..efaff85 --- /dev/null +++ b/addon/models/maintenance-subject.js @@ -0,0 +1,78 @@ +import Model, { attr } from '@ember-data/model'; +import { computed } from '@ember/object'; +import { format as formatDate, isValid as isValidDate, formatDistanceToNow } from 'date-fns'; + +/** + * Abstract base model for polymorphic maintenance subjects. + * Concrete types: maintenance-subject-vehicle, maintenance-subject-equipment + * + * The backend stores the type as a PolymorphicType cast string, e.g.: + * 'fleet-ops:vehicle' -> maintenance-subject-vehicle + * 'fleet-ops:equipment' -> maintenance-subject-equipment + */ +export default class MaintenanceSubjectModel extends Model { + /** @ids */ + @attr('string') uuid; + @attr('string') public_id; + @attr('string') company_uuid; + + /** @attributes */ + @attr('string') name; + @attr('string') display_name; + @attr('string') type; + @attr('string') status; + @attr('string') photo_url; + @attr('string') slug; + + /** @dates */ + @attr('date') deleted_at; + @attr('date') created_at; + @attr('date') updated_at; + + /** @computed */ + @computed('name', 'display_name', 'public_id') get displayName() { + return this.display_name || this.name || this.public_id; + } + + @computed('updated_at') get updatedAgo() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDistanceToNow(this.updated_at); + } + + @computed('updated_at') get updatedAt() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDate(this.updated_at, 'yyyy-MM-dd HH:mm'); + } + + @computed('updated_at') get updatedAtShort() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDate(this.updated_at, 'dd, MMM'); + } + + @computed('created_at') get createdAgo() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDistanceToNow(this.created_at); + } + + @computed('created_at') get createdAt() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDate(this.created_at, 'yyyy-MM-dd HH:mm'); + } + + @computed('created_at') get createdAtShort() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDate(this.created_at, 'dd, MMM'); + } +} diff --git a/addon/models/maintenance.js b/addon/models/maintenance.js index cc665ac..8bbaf83 100644 --- a/addon/models/maintenance.js +++ b/addon/models/maintenance.js @@ -4,13 +4,18 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class MaintenanceModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') work_order_uuid; - @attr('string') maintainable_type; - @attr('string') maintainable_uuid; - @attr('string') performed_by_type; - @attr('string') performed_by_uuid; + + /** @polymorphic relationships */ + @belongsTo('maintenance-subject', { polymorphic: true, async: false }) maintainable; + @belongsTo('facilitator', { polymorphic: true, async: false }) performed_by; + /** @computed names — server-side convenience fields (read-only) */ + @attr('string') maintainable_name; + @attr('string') performed_by_name; + @attr('string') work_order_subject; /** @relationships */ @belongsTo('work-order', { async: false }) work_order; @@ -25,14 +30,21 @@ export default class MaintenanceModel extends Model { @attr('string') summary; @attr('string') notes; @attr('raw') line_items; - @attr('number') labor_cost; - @attr('number') parts_cost; - @attr('number') tax; - @attr('number') total_cost; + @attr('string') labor_cost; + @attr('string') parts_cost; + @attr('string') tax; + @attr('string') total_cost; + @attr('string') currency; @attr('raw') attachments; @attr('raw') meta; @attr('string') slug; + /** @server-computed (read-only appended attributes) */ + @attr('number') duration_hours; + @attr('boolean') is_overdue; + @attr('number') days_until_due; + @attr('raw') cost_breakdown; + /** @dates */ @attr('date') scheduled_at; @attr('date') started_at; diff --git a/addon/models/part.js b/addon/models/part.js index 25b5401..decea97 100644 --- a/addon/models/part.js +++ b/addon/models/part.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class PartModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') vendor_uuid; @@ -26,16 +27,22 @@ export default class PartModel extends Model { @attr('string') barcode; @attr('string') description; @attr('number') quantity_on_hand; - @attr('number') unit_cost; - @attr('number') msrp; + @attr('string') unit_cost; + @attr('string') msrp; + @attr('string') currency; @attr('string') type; @attr('string') status; @attr('raw') specs; @attr('raw') meta; @attr('string') slug; + /** @server-computed (read-only appended attributes) */ @attr('string') vendor_name; @attr('string') warranty_name; @attr('string') photo_url; + @attr('string') total_value; + @attr('boolean') is_in_stock; + @attr('boolean') is_low_stock; + @attr('string') asset_name; /** @dates */ @attr('date') deleted_at; diff --git a/addon/models/sensor.js b/addon/models/sensor.js index 84f2a86..911d1ac 100644 --- a/addon/models/sensor.js +++ b/addon/models/sensor.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class SensorModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') telematic_uuid; @@ -21,7 +22,6 @@ export default class SensorModel extends Model { /** @attributes */ @attr('string') name; - @attr('string') photo_url; @attr('string') internal_id; @attr('string') type; @attr('string') serial_number; @@ -29,11 +29,9 @@ export default class SensorModel extends Model { @attr('string') imsi; @attr('string') firmware_version; @attr('string') unit; - @attr('string') threshold_status; @attr('number') min_threshold; @attr('number') max_threshold; @attr('boolean') threshold_inclusive; - @attr('boolean') is_active; @attr('string') last_value; @attr('number') report_frequency_sec; @attr('point') last_position; @@ -42,6 +40,11 @@ export default class SensorModel extends Model { @attr('string') slug; @attr('string', { defaultValue: 'inactive' }) status; + /** @server-computed (read-only appended attributes) */ + @attr('string') photo_url; + @attr('boolean') is_active; + @attr('string') threshold_status; + /** @dates */ @attr('date') last_reading_at; @attr('date') deleted_at; diff --git a/addon/models/warranty.js b/addon/models/warranty.js index e6a7bfb..85fb37a 100644 --- a/addon/models/warranty.js +++ b/addon/models/warranty.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class WarrantyModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') subject_type; @@ -21,7 +22,15 @@ export default class WarrantyModel extends Model { @attr('raw') policy; @attr('raw') meta; @attr('string') slug; + + /** @server-computed (read-only appended attributes) */ @attr('string') vendor_name; + @attr('string') subject_name; + @attr('boolean') is_active; + @attr('boolean') is_expired; + @attr('number') days_remaining; + @attr('string') coverage_summary; + @attr('string') status; /** @dates */ @attr('date') start_date; diff --git a/addon/models/work-order.js b/addon/models/work-order.js index 4b8488d..c60c454 100644 --- a/addon/models/work-order.js +++ b/addon/models/work-order.js @@ -1,19 +1,20 @@ -import Model, { attr } from '@ember-data/model'; +import Model, { attr, belongsTo } from '@ember-data/model'; import { computed } from '@ember/object'; import { format as formatDate, isValid as isValidDate, formatDistanceToNow } from 'date-fns'; export default class WorkOrderModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; - @attr('string') target_type; - @attr('string') target_uuid; - @attr('string') assignee_type; - @attr('string') assignee_uuid; + @attr('string') schedule_uuid; - /** @relationships */ - // Note: relationships would be polymorphic (target, assignee) - // but not explicitly defined in Ember Data for morphTo + /** @polymorphic relationships */ + @belongsTo('maintenance-subject', { polymorphic: true, async: false }) target; + @belongsTo('facilitator', { polymorphic: true, async: false }) assignee; + /** @computed names — server-side convenience fields (read-only) */ + @attr('string') target_name; + @attr('string') assignee_name; /** @attributes */ @attr('string') code; @@ -22,8 +23,20 @@ export default class WorkOrderModel extends Model { @attr('string') priority; @attr('string') instructions; @attr('raw') checklist; + @attr('string') estimated_cost; + @attr('string') approved_budget; + @attr('string') actual_cost; + @attr('string') currency; + @attr('raw') cost_breakdown; + @attr('string') cost_center; + @attr('string') budget_code; @attr('raw') meta; - @attr('string') slug; + + /** @server-computed (read-only appended attributes) */ + @attr('boolean') is_overdue; + @attr('number') days_until_due; + @attr('number') completion_percentage; + @attr('number') estimated_duration; /** @dates */ @attr('date') opened_at; diff --git a/addon/serializers/maintenance-schedule.js b/addon/serializers/maintenance-schedule.js new file mode 100644 index 0000000..9225d0d --- /dev/null +++ b/addon/serializers/maintenance-schedule.js @@ -0,0 +1,145 @@ +import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; +import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; + +/** + * Maps every type string the backend may send for the subject relationship to + * the Ember Data model name for the concrete maintenance-subject subtype. + * + * The fixed MaintenanceSchedule resource now injects `subject_type` using the bare + * class basename slug (e.g. 'maintenance-subject-vehicle'). We also keep short-form + * and full-class-name keys for backwards compatibility. + */ +const MAINTENANCE_SUBJECT_TYPE_MAP = { + // Canonical form injected by the fixed resource (bare slug) + 'maintenance-subject-vehicle': 'maintenance-subject-vehicle', + 'maintenance-subject-equipment': 'maintenance-subject-equipment', + // Short-form keys emitted by toEmberResourceType() on subject_type field + 'fleet-ops:vehicle': 'maintenance-subject-vehicle', + 'fleet-ops:equipment': 'maintenance-subject-equipment', + // Bare model names + vehicle: 'maintenance-subject-vehicle', + equipment: 'maintenance-subject-equipment', + // Full PHP class names (legacy / backwards-compat) + 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', +}; + +/** + * Maps every type string the backend may send for the default_assignee relationship to + * the Ember Data model name for the concrete facilitator subtype. + * + * The fixed MaintenanceSchedule resource now injects `facilitator_type` using the bare + * class basename slug (e.g. 'facilitator-vendor'). We also keep short-form and + * full-class-name keys for backwards compatibility. + */ +const FACILITATOR_TYPE_MAP = { + // Canonical form injected by the fixed resource (bare slug) + 'facilitator-vendor': 'facilitator-vendor', + 'facilitator-contact': 'facilitator-contact', + 'facilitator-integrated-vendor': 'facilitator-integrated-vendor', + // Short-form keys emitted by toEmberResourceType() on default_assignee_type field + 'fleet-ops:vendor': 'facilitator-vendor', + 'fleet-ops:contact': 'facilitator-contact', + 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', + 'fleet-ops:driver': 'facilitator-contact', + // Bare model names + vendor: 'facilitator-vendor', + contact: 'facilitator-contact', + driver: 'facilitator-contact', + // Full PHP class names (legacy / backwards-compat) + 'Fleetbase\\FleetOps\\Models\\Vendor': 'facilitator-vendor', + 'Fleetbase\\FleetOps\\Models\\Contact': 'facilitator-contact', + 'Fleetbase\\FleetOps\\Models\\IntegratedVendor': 'facilitator-integrated-vendor', + 'Fleetbase\\FleetOps\\Models\\Driver': 'facilitator-contact', +}; + +/** + * Maps the Ember Data model name back to the shorthand type string that the + * backend accepts on create/update. The server converts "fleet-ops:vehicle" + * to the correct PHP namespace internally. + */ +const SUBJECT_EMBER_TO_SHORTHAND = { + 'maintenance-subject-vehicle': 'fleet-ops:vehicle', + 'maintenance-subject-equipment': 'fleet-ops:equipment', + // Raw model names (e.g. when subject is set directly from vehicle-actions) + vehicle: 'fleet-ops:vehicle', + equipment: 'fleet-ops:equipment', +}; + +const FACILITATOR_EMBER_TO_SHORTHAND = { + 'facilitator-vendor': 'fleet-ops:vendor', + 'facilitator-contact': 'fleet-ops:contact', + 'facilitator-integrated-vendor': 'fleet-ops:integrated-vendor', + // Raw model names + vendor: 'fleet-ops:vendor', + contact: 'fleet-ops:contact', + driver: 'fleet-ops:contact', + user: 'Auth:User', +}; + +export default class MaintenanceScheduleSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + /** + * The subject and default_assignee relationships are always embedded in the + * server response via the MaintenanceSchedule resource transformer. + */ + get attrs() { + return { + subject: { embedded: 'always' }, + default_assignee: { embedded: 'always' }, + }; + } + + /** + * Normalise polymorphic type strings from the backend into Ember Data model names. + * + * For `subject`: the resource injects `subject_type` (e.g. 'maintenance-subject-vehicle') + * into the embedded object. We read that first; if missing we fall back to `subject_type` + * on the parent hash. + * + * For `default_assignee`: the resource injects `facilitator_type` (e.g. 'facilitator-vendor') + * into the embedded object. We read that first; if missing we fall back to + * `default_assignee_type` on the parent hash. + */ + normalizePolymorphicType(resourceHash, relationship) { + const key = relationship.key; + const typeKey = `${key}_type`; + + if (key === 'subject') { + // Prefer the injected subject_type over the raw subject_type field + const subjectType = resourceHash['subject_type'] ?? resourceHash[typeKey]; + if (subjectType) { + resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[subjectType] ?? subjectType; + } + } else if (key === 'default_assignee') { + // Prefer the injected facilitator_type over the raw default_assignee_type field + const facilitatorType = resourceHash['facilitator_type'] ?? resourceHash[typeKey]; + if (facilitatorType) { + resourceHash[typeKey] = FACILITATOR_TYPE_MAP[facilitatorType] ?? facilitatorType; + } + } + + return super.normalizePolymorphicType ? super.normalizePolymorphicType(...arguments) : resourceHash[typeKey]; + } + + /** + * Serialise the polymorphic type back to the shorthand string the backend + * expects on create / update (e.g. "fleet-ops:vehicle"). + */ + serializePolymorphicType(snapshot, json, relationship) { + const key = relationship.key; + const belongsTo = snapshot.belongsTo(key); + + if (!belongsTo) { + json[`${key}_type`] = null; + return; + } + + const modelName = belongsTo.modelName; + + if (key === 'subject') { + json[`${key}_type`] = SUBJECT_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; + } else if (key === 'default_assignee') { + json[`${key}_type`] = FACILITATOR_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; + } + } +} diff --git a/addon/serializers/maintenance.js b/addon/serializers/maintenance.js index 278a63b..06649a5 100644 --- a/addon/serializers/maintenance.js +++ b/addon/serializers/maintenance.js @@ -1,4 +1,145 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; -export default class MaintenanceSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {} +/** + * Maps every type string the backend may send for the maintainable relationship to + * the Ember Data model name for the concrete maintenance-subject subtype. + * + * The fixed Maintenance resource now injects `subject_type` using the bare class + * basename slug (e.g. 'maintenance-subject-vehicle'). We also keep short-form and + * full-class-name keys for backwards compatibility with existing records. + */ +const MAINTENANCE_SUBJECT_TYPE_MAP = { + // Canonical form injected by the fixed resource (bare slug) + 'maintenance-subject-vehicle': 'maintenance-subject-vehicle', + 'maintenance-subject-equipment': 'maintenance-subject-equipment', + // Short-form keys emitted by toEmberResourceType() on maintainable_type field + 'fleet-ops:vehicle': 'maintenance-subject-vehicle', + 'fleet-ops:equipment': 'maintenance-subject-equipment', + // Bare model names + vehicle: 'maintenance-subject-vehicle', + equipment: 'maintenance-subject-equipment', + // Full PHP class names (legacy / backwards-compat) + 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', +}; + +/** + * Maps every type string the backend may send for the performed_by relationship to + * the Ember Data model name for the concrete facilitator subtype. + * + * The fixed Maintenance resource now injects `facilitator_type` using the bare class + * basename slug (e.g. 'facilitator-vendor'). We also keep short-form and full-class-name + * keys for backwards compatibility. + */ +const FACILITATOR_TYPE_MAP = { + // Canonical form injected by the fixed resource (bare slug) + 'facilitator-vendor': 'facilitator-vendor', + 'facilitator-contact': 'facilitator-contact', + 'facilitator-integrated-vendor': 'facilitator-integrated-vendor', + // Short-form keys emitted by toEmberResourceType() on performed_by_type field + 'fleet-ops:vendor': 'facilitator-vendor', + 'fleet-ops:contact': 'facilitator-contact', + 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', + 'fleet-ops:driver': 'facilitator-contact', + // Bare model names + vendor: 'facilitator-vendor', + contact: 'facilitator-contact', + driver: 'facilitator-contact', + // Full PHP class names (legacy / backwards-compat) + 'Fleetbase\\FleetOps\\Models\\Vendor': 'facilitator-vendor', + 'Fleetbase\\FleetOps\\Models\\Contact': 'facilitator-contact', + 'Fleetbase\\FleetOps\\Models\\IntegratedVendor': 'facilitator-integrated-vendor', + 'Fleetbase\\FleetOps\\Models\\Driver': 'facilitator-contact', +}; + +/** + * Maps the Ember Data model name back to the shorthand type string the + * backend expects on create / update (written into maintainable_type / + * performed_by_type via the PolymorphicType cast → getMutationType()). + */ +const MAINTAINABLE_EMBER_TO_SHORTHAND = { + 'maintenance-subject-vehicle': 'fleet-ops:vehicle', + 'maintenance-subject-equipment': 'fleet-ops:equipment', + // Raw model names (e.g. when maintainable is set directly from vehicle-actions) + vehicle: 'fleet-ops:vehicle', + equipment: 'fleet-ops:equipment', +}; + +const PERFORMED_BY_EMBER_TO_SHORTHAND = { + 'facilitator-vendor': 'fleet-ops:vendor', + 'facilitator-contact': 'fleet-ops:contact', + 'facilitator-integrated-vendor': 'fleet-ops:integrated-vendor', + // Raw model names + vendor: 'fleet-ops:vendor', + contact: 'fleet-ops:contact', + driver: 'fleet-ops:contact', + user: 'Auth:User', +}; + +export default class MaintenanceSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + /** + * maintainable, performed_by, work_order and custom_field_values are all + * embedded in the server response via the Maintenance resource transformer. + */ + get attrs() { + return { + maintainable: { embedded: 'always' }, + performed_by: { embedded: 'always' }, + work_order: { embedded: 'always' }, + custom_field_values: { embedded: 'always' }, + }; + } + + /** + * Normalise polymorphic type strings from the backend into Ember Data model names. + * + * For `maintainable`: the resource injects `subject_type` (e.g. 'maintenance-subject-vehicle') + * into the embedded object. We read that first; if missing we fall back to `maintainable_type`. + * + * For `performed_by`: the resource injects `facilitator_type` (e.g. 'facilitator-vendor') + * into the embedded object. We read that first; if missing we fall back to `performed_by_type`. + */ + normalizePolymorphicType(resourceHash, relationship) { + const key = relationship.key; + const typeKey = `${key}_type`; + + if (key === 'maintainable') { + // Prefer the injected subject_type over the raw maintainable_type field + const subjectType = resourceHash['subject_type'] ?? resourceHash[typeKey]; + if (subjectType) { + resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[subjectType] ?? subjectType; + } + } else if (key === 'performed_by') { + // Prefer the injected facilitator_type over the raw performed_by_type field + const facilitatorType = resourceHash['facilitator_type'] ?? resourceHash[typeKey]; + if (facilitatorType) { + resourceHash[typeKey] = FACILITATOR_TYPE_MAP[facilitatorType] ?? facilitatorType; + } + } + + return super.normalizePolymorphicType ? super.normalizePolymorphicType(...arguments) : resourceHash[typeKey]; + } + + /** + * Serialise the polymorphic type back to the shorthand string the backend + * expects on create / update. + */ + serializePolymorphicType(snapshot, json, relationship) { + const key = relationship.key; + const belongsTo = snapshot.belongsTo(key); + + if (!belongsTo) { + json[`${key}_type`] = null; + return; + } + + const modelName = belongsTo.modelName; + + if (key === 'maintainable') { + json[`${key}_type`] = MAINTAINABLE_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; + } else if (key === 'performed_by') { + json[`${key}_type`] = PERFORMED_BY_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; + } + } +} diff --git a/addon/serializers/vendor.js b/addon/serializers/vendor.js index 24cb17c..1aec886 100644 --- a/addon/serializers/vendor.js +++ b/addon/serializers/vendor.js @@ -3,13 +3,20 @@ import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; export default class VendorSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { /** - * Embedded relationship attributes + * Embedded relationship attributes. + * + * `personnels` must be declared as embedded so that the EmbeddedRecordsMixin + * resolves each record using the declared `@hasMany('contact')` model type + * rather than attempting to look up the model by the raw `type` field on + * each payload object (which carries a backend STI discriminator such as + * `fliit_contact` that does not exist in the Ember Data model registry). * * @var {Object} */ get attrs() { return { place: { embedded: 'always' }, + personnels: { embedded: 'always' }, custom_field_values: { embedded: 'always' }, }; } diff --git a/addon/serializers/work-order.js b/addon/serializers/work-order.js index 6897d0f..22d6274 100644 --- a/addon/serializers/work-order.js +++ b/addon/serializers/work-order.js @@ -1,4 +1,143 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; -export default class WorkOrderSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {} +/** + * Maps every type string the backend may send for the target relationship to + * the Ember Data model name for the concrete maintenance-subject subtype. + * + * The fixed resource now injects `subject_type` using the bare class basename slug + * (e.g. 'maintenance-subject-vehicle'). We also keep short-form and full-class-name + * keys for backwards compatibility. + */ +const MAINTENANCE_SUBJECT_TYPE_MAP = { + // Canonical form injected by the fixed resource (bare slug) + 'maintenance-subject-vehicle': 'maintenance-subject-vehicle', + 'maintenance-subject-equipment': 'maintenance-subject-equipment', + // Short-form keys emitted by toEmberResourceType() on target_type field + 'fleet-ops:vehicle': 'maintenance-subject-vehicle', + 'fleet-ops:equipment': 'maintenance-subject-equipment', + // Bare model names (e.g. when target is a raw vehicle record) + vehicle: 'maintenance-subject-vehicle', + equipment: 'maintenance-subject-equipment', + // Full PHP class names (legacy / backwards-compat) + 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', +}; + +/** + * Maps every type string the backend may send for the assignee relationship to + * the Ember Data model name for the concrete facilitator subtype. + * + * The fixed resource now injects `facilitator_type` using the bare class basename slug + * (e.g. 'facilitator-vendor'). We also keep short-form and full-class-name keys. + */ +const FACILITATOR_TYPE_MAP = { + // Canonical form injected by the fixed resource (bare slug) + 'facilitator-vendor': 'facilitator-vendor', + 'facilitator-contact': 'facilitator-contact', + 'facilitator-integrated-vendor': 'facilitator-integrated-vendor', + // Short-form keys emitted by toEmberResourceType() on assignee_type field + 'fleet-ops:vendor': 'facilitator-vendor', + 'fleet-ops:contact': 'facilitator-contact', + 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', + 'fleet-ops:driver': 'facilitator-contact', + // Bare model names + vendor: 'facilitator-vendor', + contact: 'facilitator-contact', + driver: 'facilitator-contact', + // Full PHP class names (legacy / backwards-compat) + 'Fleetbase\\FleetOps\\Models\\Vendor': 'facilitator-vendor', + 'Fleetbase\\FleetOps\\Models\\Contact': 'facilitator-contact', + 'Fleetbase\\FleetOps\\Models\\IntegratedVendor': 'facilitator-integrated-vendor', + 'Fleetbase\\FleetOps\\Models\\Driver': 'facilitator-contact', +}; + +/** + * Maps the Ember Data model name back to the shorthand type string the + * backend expects on create / update (written into target_type / assignee_type + * via the PolymorphicType cast which calls getMutationType()). + */ +const TARGET_EMBER_TO_SHORTHAND = { + 'maintenance-subject-vehicle': 'fleet-ops:vehicle', + 'maintenance-subject-equipment': 'fleet-ops:equipment', + // Raw model names (e.g. when target is set directly from vehicle-actions) + vehicle: 'fleet-ops:vehicle', + equipment: 'fleet-ops:equipment', +}; + +const ASSIGNEE_EMBER_TO_SHORTHAND = { + 'facilitator-vendor': 'fleet-ops:vendor', + 'facilitator-contact': 'fleet-ops:contact', + 'facilitator-integrated-vendor': 'fleet-ops:integrated-vendor', + // Raw model names (e.g. when assignee is set directly from a ModelSelect) + vendor: 'fleet-ops:vendor', + contact: 'fleet-ops:contact', + driver: 'fleet-ops:contact', + user: 'Auth:User', +}; + +export default class WorkOrderSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + /** + * The target and assignee relationships are always embedded in the server + * response via the WorkOrder resource transformer. + */ + get attrs() { + return { + target: { embedded: 'always' }, + assignee: { embedded: 'always' }, + custom_field_values: { embedded: 'always' }, + }; + } + + /** + * Normalise polymorphic type strings from the backend into Ember Data model names. + * + * For `target`: the resource injects `subject_type` (e.g. 'maintenance-subject-vehicle') + * into the embedded object. We read that first; if missing we fall back to `target_type`. + * + * For `assignee`: the resource injects `facilitator_type` (e.g. 'facilitator-vendor') + * into the embedded object. We read that first; if missing we fall back to `assignee_type`. + */ + normalizePolymorphicType(resourceHash, relationship) { + const key = relationship.key; + const typeKey = `${key}_type`; + + if (key === 'target') { + // Prefer the injected subject_type over the raw target_type field + const subjectType = resourceHash['subject_type'] ?? resourceHash[typeKey]; + if (subjectType) { + resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[subjectType] ?? subjectType; + } + } else if (key === 'assignee') { + // Prefer the injected facilitator_type over the raw assignee_type field + const facilitatorType = resourceHash['facilitator_type'] ?? resourceHash[typeKey]; + if (facilitatorType) { + resourceHash[typeKey] = FACILITATOR_TYPE_MAP[facilitatorType] ?? facilitatorType; + } + } + + return super.normalizePolymorphicType ? super.normalizePolymorphicType(...arguments) : resourceHash[typeKey]; + } + + /** + * Serialise the polymorphic type back to the shorthand string the backend + * expects on create / update. + */ + serializePolymorphicType(snapshot, json, relationship) { + const key = relationship.key; + const belongsTo = snapshot.belongsTo(key); + + if (!belongsTo) { + json[`${key}_type`] = null; + return; + } + + const modelName = belongsTo.modelName; + + if (key === 'target') { + json[`${key}_type`] = TARGET_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; + } else if (key === 'assignee') { + json[`${key}_type`] = ASSIGNEE_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; + } + } +} diff --git a/app/models/maintenance-schedule.js b/app/models/maintenance-schedule.js new file mode 100644 index 0000000..5bc42d0 --- /dev/null +++ b/app/models/maintenance-schedule.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/models/maintenance-schedule'; diff --git a/app/models/maintenance-subject-equipment.js b/app/models/maintenance-subject-equipment.js new file mode 100644 index 0000000..cbca1e1 --- /dev/null +++ b/app/models/maintenance-subject-equipment.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/models/maintenance-subject-equipment'; diff --git a/app/models/maintenance-subject-vehicle.js b/app/models/maintenance-subject-vehicle.js new file mode 100644 index 0000000..bc889fa --- /dev/null +++ b/app/models/maintenance-subject-vehicle.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/models/maintenance-subject-vehicle'; diff --git a/app/models/maintenance-subject.js b/app/models/maintenance-subject.js new file mode 100644 index 0000000..1c1eb3b --- /dev/null +++ b/app/models/maintenance-subject.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/models/maintenance-subject'; diff --git a/app/serializers/maintenance-schedule.js b/app/serializers/maintenance-schedule.js new file mode 100644 index 0000000..129b59f --- /dev/null +++ b/app/serializers/maintenance-schedule.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/serializers/maintenance-schedule';