From 70f5ac5f06e7b42bd65a079c002243c1553c8a6a Mon Sep 17 00:00:00 2001 From: Ronald A Richardson Date: Mon, 30 Mar 2026 04:45:39 -0400 Subject: [PATCH 01/14] feat: add schedule, schedule-item and availability relationships to Driver model Connects the Driver Ember Data model to the core-api scheduling system by adding three hasMany relationships: - schedules: polymorphic Schedule records where the driver is the subject - schedule_items: individual shift records (ScheduleItem) where the driver is the assignee (assignee_type='driver', assignee_uuid=driver.id) - availabilities: ScheduleAvailability records for time-off and preferred working hours These relationships are required by: 1. The new driver/schedule panel component which renders a per-driver FullCalendar shift management view 2. The Intelligent Order Allocation Engine AllocationPayloadBuilder which queries active schedule_items to inject shift time_window constraints into the VROOM optimization payload Refs: fleetbase/fleetops#214 --- addon/models/driver.js | 5 +++++ 1 file changed, 5 insertions(+) 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; From b9e540c2b707034a1c051a6450a76d66cb371e16 Mon Sep 17 00:00:00 2001 From: Fleetbase Dev Date: Tue, 31 Mar 2026 00:34:27 -0400 Subject: [PATCH 02/14] fix(serializers): normalize personnels type discriminator in VendorSerializer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend stores STI discriminator values (e.g. `fliit_contact`) in the contacts.type column and passes them straight through in the Contact HTTP Resource. Ember Data's VendorModel declares `@hasMany('contact') personnels` and tries to resolve each embedded record by its `type` field as a model registry name — finding `fliit_contact` instead of `contact` and throwing: Assertion Failed: Encountered a relationship identifier with type 'fliit_contact' for the hasMany relationship 'personnels' on , Expected an identifier with type 'contact'. Fix: - Add `personnels: { embedded: 'always' }` to VendorSerializer attrs so the inline objects are processed through the normalize pipeline. - Override `normalize()` in VendorSerializer to strip known package prefixes (fliit_, fleet_ops_, fleetops_) from each personnel's `type` field, converting it to the bare Ember model name (e.g. `contact`). - Preserve the original discriminator value under `subtype` so application code that depends on it (role checks, routing, conditional rendering) is completely unaffected. - Add `@attr('string') subtype` to ContactModel to expose the preserved value. No backend changes required. --- addon/models/contact.js | 1 + addon/serializers/vendor.js | 80 ++++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/addon/models/contact.js b/addon/models/contact.js index 02720b7..fd00f2c 100644 --- a/addon/models/contact.js +++ b/addon/models/contact.js @@ -27,6 +27,7 @@ export default class ContactModel extends Model { @attr('string') address; @attr('string') address_street; @attr('string') type; + @attr('string') subtype; @attr('string', { defaultValue: 'https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png', }) diff --git a/addon/serializers/vendor.js b/addon/serializers/vendor.js index 24cb17c..5965c5b 100644 --- a/addon/serializers/vendor.js +++ b/addon/serializers/vendor.js @@ -1,16 +1,94 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; +/** + * Strips a package prefix from a raw API type discriminator and returns the + * bare Ember Data model name. + * + * The backend stores STI discriminator values such as `fliit_contact`, + * `fliit_vendor`, `fliit_client`, etc. Ember Data's model registry only + * knows the bare name (`contact`, `vendor`, …). This helper converts the + * raw value while preserving the original under `subtype` so application + * code that depends on the discriminator (e.g. role/routing logic) is + * unaffected. + * + * Examples: + * fliit_contact → contact + * fliit_vendor → vendor + * fliit_client → client + * contact → contact (already bare — returned as-is) + * + * @param {string} rawType + * @returns {string} + */ +function toEmberModelType(rawType) { + if (typeof rawType !== 'string' || !rawType) { + return rawType; + } + + // Already a bare name with no known package prefix + if (!rawType.includes('_')) { + return rawType; + } + + // Known package prefixes used by Fleetbase extensions + const knownPrefixes = ['fliit_', 'fleet_ops_', 'fleetops_']; + for (const prefix of knownPrefixes) { + if (rawType.startsWith(prefix)) { + return rawType.slice(prefix.length); + } + } + + return rawType; +} + export default class VendorSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { /** - * Embedded relationship attributes + * Embedded relationship attributes. + * + * `personnels` is embedded so that each contact hash is processed through + * the ContactSerializer (and this serializer's normalize hook) rather than + * being treated as a bare id array by Ember Data. * * @var {Object} */ get attrs() { return { place: { embedded: 'always' }, + personnels: { embedded: 'always' }, custom_field_values: { embedded: 'always' }, }; } + + /** + * Normalizes a single resource hash returned by the API. + * + * Walks the `personnels` array and rewrites each record's `type` field + * from the raw backend discriminator (e.g. `fliit_contact`) to the bare + * Ember Data model name (`contact`). The original discriminator value is + * preserved under `subtype` so downstream code that depends on it + * (role checks, conditional rendering, etc.) continues to work. + * + * @param {DS.Model} modelClass + * @param {Object} resourceHash + * @param {string} prop + * @returns {Object} + */ + normalize(modelClass, resourceHash, prop) { + if (Array.isArray(resourceHash.personnels)) { + resourceHash.personnels = resourceHash.personnels.map((personnel) => { + if (personnel && typeof personnel === 'object' && personnel.type) { + const emberType = toEmberModelType(personnel.type); + if (emberType !== personnel.type) { + // Preserve the original discriminator so app logic can still use it + personnel.subtype = personnel.type; + personnel.type = emberType; + } + } + return personnel; + }); + } + + return super.normalize(modelClass, resourceHash, prop); + } } From 10c48494c64cc7f2401f71c17b9c4343f0f3bdd0 Mon Sep 17 00:00:00 2001 From: Fleetbase Dev Date: Tue, 31 Mar 2026 00:40:20 -0400 Subject: [PATCH 03/14] fix(serializers): resolve personnels hasMany as embedded contacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The VendorModel declares `@hasMany('contact') personnels` but the VendorSerializer did not declare personnels as embedded. Without the embedded declaration, Ember Data attempts to resolve each inline personnel object by reading its `type` field as a model registry name. The backend stores an STI discriminator in that field (e.g. `fliit_contact`) which does not exist in the Ember Data registry, causing: Assertion Failed: Encountered a relationship identifier with type 'fliit_contact' for the hasMany relationship 'personnels' on Fix: add `personnels: { embedded: 'always' }` to VendorSerializer attrs. The EmbeddedRecordsMixin then resolves each record using the declared relationship model type (`contact`) and the `type` attribute on each payload object is treated as a plain domain attribute — untouched. Also reverts the unnecessary `subtype` attr added to ContactModel in the previous commit. --- addon/models/contact.js | 1 - addon/serializers/vendor.js | 81 +++---------------------------------- 2 files changed, 5 insertions(+), 77 deletions(-) diff --git a/addon/models/contact.js b/addon/models/contact.js index fd00f2c..02720b7 100644 --- a/addon/models/contact.js +++ b/addon/models/contact.js @@ -27,7 +27,6 @@ export default class ContactModel extends Model { @attr('string') address; @attr('string') address_street; @attr('string') type; - @attr('string') subtype; @attr('string', { defaultValue: 'https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png', }) diff --git a/addon/serializers/vendor.js b/addon/serializers/vendor.js index 5965c5b..1aec886 100644 --- a/addon/serializers/vendor.js +++ b/addon/serializers/vendor.js @@ -1,54 +1,15 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; -/** - * Strips a package prefix from a raw API type discriminator and returns the - * bare Ember Data model name. - * - * The backend stores STI discriminator values such as `fliit_contact`, - * `fliit_vendor`, `fliit_client`, etc. Ember Data's model registry only - * knows the bare name (`contact`, `vendor`, …). This helper converts the - * raw value while preserving the original under `subtype` so application - * code that depends on the discriminator (e.g. role/routing logic) is - * unaffected. - * - * Examples: - * fliit_contact → contact - * fliit_vendor → vendor - * fliit_client → client - * contact → contact (already bare — returned as-is) - * - * @param {string} rawType - * @returns {string} - */ -function toEmberModelType(rawType) { - if (typeof rawType !== 'string' || !rawType) { - return rawType; - } - - // Already a bare name with no known package prefix - if (!rawType.includes('_')) { - return rawType; - } - - // Known package prefixes used by Fleetbase extensions - const knownPrefixes = ['fliit_', 'fleet_ops_', 'fleetops_']; - for (const prefix of knownPrefixes) { - if (rawType.startsWith(prefix)) { - return rawType.slice(prefix.length); - } - } - - return rawType; -} - export default class VendorSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { /** * Embedded relationship attributes. * - * `personnels` is embedded so that each contact hash is processed through - * the ContactSerializer (and this serializer's normalize hook) rather than - * being treated as a bare id array by Ember Data. + * `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} */ @@ -59,36 +20,4 @@ export default class VendorSerializer extends ApplicationSerializer.extend(Embed custom_field_values: { embedded: 'always' }, }; } - - /** - * Normalizes a single resource hash returned by the API. - * - * Walks the `personnels` array and rewrites each record's `type` field - * from the raw backend discriminator (e.g. `fliit_contact`) to the bare - * Ember Data model name (`contact`). The original discriminator value is - * preserved under `subtype` so downstream code that depends on it - * (role checks, conditional rendering, etc.) continues to work. - * - * @param {DS.Model} modelClass - * @param {Object} resourceHash - * @param {string} prop - * @returns {Object} - */ - normalize(modelClass, resourceHash, prop) { - if (Array.isArray(resourceHash.personnels)) { - resourceHash.personnels = resourceHash.personnels.map((personnel) => { - if (personnel && typeof personnel === 'object' && personnel.type) { - const emberType = toEmberModelType(personnel.type); - if (emberType !== personnel.type) { - // Preserve the original discriminator so app logic can still use it - personnel.subtype = personnel.type; - personnel.type = emberType; - } - } - return personnel; - }); - } - - return super.normalize(modelClass, resourceHash, prop); - } } From 93c0494f78d2885980652b0d935eaa85889950f5 Mon Sep 17 00:00:00 2001 From: Ronald Richardson Date: Tue, 31 Mar 2026 03:26:23 -0400 Subject: [PATCH 04/14] feat(models): add MaintenanceSchedule Ember Data model Adds the maintenance-schedule model to the fleetops-data package so the Ember Data store can resolve 'maintenance-schedule' queries from the maintenance module. Files added: - addon/models/maintenance-schedule.js: full model with all interval fields (time/distance/engine-hours), next-due thresholds, default assignee attrs, status, subject polymorphic fields, and computed date helpers (updatedAt, createdAt, nextDueAt, isActive, isPaused) - app/models/maintenance-schedule.js: re-export shim following the same pattern as all other models in this package --- addon/models/maintenance-schedule.js | 108 +++++++++++++++++++++++++++ app/models/maintenance-schedule.js | 1 + 2 files changed, 109 insertions(+) create mode 100644 addon/models/maintenance-schedule.js create mode 100644 app/models/maintenance-schedule.js diff --git a/addon/models/maintenance-schedule.js b/addon/models/maintenance-schedule.js new file mode 100644 index 0000000..bb106d2 --- /dev/null +++ b/addon/models/maintenance-schedule.js @@ -0,0 +1,108 @@ +import Model, { attr } 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') public_id; + @attr('string') company_uuid; + + /** @polymorphic subject (the asset this schedule applies to) */ + @attr('string') subject_type; + @attr('string') subject_uuid; + @attr('string') subject_name; + + /** @attributes */ + @attr('string') name; + @attr('string') type; + @attr('string') status; + + /** @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') default_assignee_type; + @attr('string') default_assignee_uuid; + + @attr('string') instructions; + @attr('raw') meta; + @attr('string') slug; + + /** @dates */ + @attr('date') deleted_at; + @attr('date') created_at; + @attr('date') updated_at; + + /** @computed */ + @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'); + } + @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('status') get isActive() { + return this.status === 'active'; + } + @computed('status') get isPaused() { + return this.status === 'paused'; + } +} 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'; From 913bb128374e691a8c1b5741a2e01daa33e16001 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 1 Apr 2026 09:04:49 +0800 Subject: [PATCH 05/14] cleanup model spacing --- addon/models/maintenance-schedule.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/addon/models/maintenance-schedule.js b/addon/models/maintenance-schedule.js index bb106d2..ba4187b 100644 --- a/addon/models/maintenance-schedule.js +++ b/addon/models/maintenance-schedule.js @@ -57,51 +57,60 @@ export default class MaintenanceScheduleModel extends Model { } 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'); } + @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('status') get isActive() { return this.status === 'active'; } + @computed('status') get isPaused() { return this.status === 'paused'; } From 1c8d2539973fc666afbf540e1a365e1172a0c39e Mon Sep 17 00:00:00 2001 From: Ronald Richardson Date: Tue, 31 Mar 2026 21:11:05 -0400 Subject: [PATCH 06/14] feat(maintenance): implement polymorphic relationships for maintenance module - Add abstract MaintenanceSubject base model with common attributes (name, status, photo_url, dates) - Add MaintenanceSubjectVehicle concrete model extending base (vehicle-specific attrs: make, model, year, plate_number, vin, odometer, engine_hours) - Add MaintenanceSubjectEquipment concrete model extending base (equipment-specific attrs: serial_number, manufacturer, purchase_price) - Add app re-export shims for all three maintenance-subject models - Update MaintenanceSchedule model: replace subject_type/subject_uuid attrs with @belongsTo('maintenance-subject', {polymorphic: true}) and @belongsTo('facilitator', {polymorphic: true}) for default_assignee - Update WorkOrder model: replace target_type/target_uuid with @belongsTo('maintenance-subject', {polymorphic: true}) and assignee_type/assignee_uuid with @belongsTo('facilitator', {polymorphic: true}) - Update Maintenance model: replace maintainable_type/maintainable_uuid with @belongsTo('maintenance-subject', {polymorphic: true}) and performed_by_type/performed_by_uuid with @belongsTo('facilitator', {polymorphic: true}) - Add MaintenanceScheduleSerializer with normalizePolymorphicType/serializePolymorphicType for subject and default_assignee - Update WorkOrderSerializer with type mapping: fleet-ops:vehicle -> maintenance-subject-vehicle, fleet-ops:equipment -> maintenance-subject-equipment - Update MaintenanceSerializer with type mapping for maintainable and performed_by relationships - All serializers use EmbeddedRecordsMixin with embedded: always for polymorphic relationships Follows the existing facilitator pattern (abstract base + concrete subtypes) used throughout the fleetops-data package. --- addon/models/maintenance-schedule.js | 68 +++++++++------- addon/models/maintenance-subject-equipment.js | 26 +++++++ addon/models/maintenance-subject-vehicle.js | 44 +++++++++++ addon/models/maintenance-subject.js | 78 +++++++++++++++++++ addon/models/maintenance.js | 9 ++- addon/models/work-order.js | 16 ++-- addon/serializers/maintenance-schedule.js | 71 +++++++++++++++++ addon/serializers/maintenance.js | 69 +++++++++++++++- addon/serializers/work-order.js | 67 +++++++++++++++- app/models/maintenance-subject-equipment.js | 1 + app/models/maintenance-subject-vehicle.js | 1 + app/models/maintenance-subject.js | 1 + app/serializers/maintenance-schedule.js | 1 + 13 files changed, 408 insertions(+), 44 deletions(-) create mode 100644 addon/models/maintenance-subject-equipment.js create mode 100644 addon/models/maintenance-subject-vehicle.js create mode 100644 addon/models/maintenance-subject.js create mode 100644 addon/serializers/maintenance-schedule.js create mode 100644 app/models/maintenance-subject-equipment.js create mode 100644 app/models/maintenance-subject-vehicle.js create mode 100644 app/models/maintenance-subject.js create mode 100644 app/serializers/maintenance-schedule.js diff --git a/addon/models/maintenance-schedule.js b/addon/models/maintenance-schedule.js index ba4187b..fe9b220 100644 --- a/addon/models/maintenance-schedule.js +++ b/addon/models/maintenance-schedule.js @@ -1,21 +1,25 @@ -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 MaintenanceScheduleModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; - /** @polymorphic subject (the asset this schedule applies to) */ - @attr('string') subject_type; - @attr('string') subject_uuid; - @attr('string') subject_name; + /** @polymorphic relationships */ + @belongsTo('maintenance-subject', { polymorphic: true, async: false }) subject; + @belongsTo('facilitator', { polymorphic: true, async: false }) default_assignee; /** @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; @@ -38,19 +42,47 @@ export default class MaintenanceScheduleModel extends Model { /** @work-order defaults */ @attr('string') default_priority; - @attr('string') default_assignee_type; - @attr('string') default_assignee_uuid; @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; @@ -92,26 +124,4 @@ export default class MaintenanceScheduleModel extends Model { } return formatDate(this.created_at, 'dd, MMM'); } - - @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('status') get isActive() { - return this.status === 'active'; - } - - @computed('status') get isPaused() { - return this.status === 'paused'; - } } 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..d1da797 100644 --- a/addon/models/maintenance.js +++ b/addon/models/maintenance.js @@ -4,13 +4,14 @@ 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; /** @relationships */ @belongsTo('work-order', { async: false }) work_order; diff --git a/addon/models/work-order.js b/addon/models/work-order.js index 4b8488d..485d6dc 100644 --- a/addon/models/work-order.js +++ b/addon/models/work-order.js @@ -1,19 +1,17 @@ -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; - - /** @relationships */ - // Note: relationships would be polymorphic (target, assignee) - // but not explicitly defined in Ember Data for morphTo + @attr('string') schedule_uuid; + + /** @polymorphic relationships */ + @belongsTo('maintenance-subject', { polymorphic: true, async: false }) target; + @belongsTo('facilitator', { polymorphic: true, async: false }) assignee; /** @attributes */ @attr('string') code; diff --git a/addon/serializers/maintenance-schedule.js b/addon/serializers/maintenance-schedule.js new file mode 100644 index 0000000..4cc317f --- /dev/null +++ b/addon/serializers/maintenance-schedule.js @@ -0,0 +1,71 @@ +import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; +import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; + +/** + * Type map for normalizing backend PolymorphicType strings to Ember Data model names. + * The backend uses Laravel's PolymorphicType cast which produces strings like 'fleet-ops:vehicle'. + */ +const MAINTENANCE_SUBJECT_TYPE_MAP = { + 'fleet-ops:vehicle': 'maintenance-subject-vehicle', + 'fleet-ops:equipment': 'maintenance-subject-equipment', +}; + +const FACILITATOR_TYPE_MAP = { + 'fleet-ops:driver': 'facilitator-contact', + 'fleet-ops:contact': 'facilitator-contact', + 'fleet-ops:vendor': 'facilitator-vendor', + 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', +}; + +export default class MaintenanceScheduleSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + get attrs() { + return { + subject: { embedded: 'always' }, + default_assignee: { embedded: 'always' }, + }; + } + + /** + * Normalize polymorphic type strings from the backend into Ember Data model names. + * Called during deserialization for each polymorphic relationship. + */ + normalizePolymorphicType(resourceHash, relationship) { + const key = relationship.key; + const typeKey = `${key}_type`; + const backendType = resourceHash[typeKey]; + + if (backendType) { + if (key === 'subject') { + resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] || backendType; + } else if (key === 'default_assignee') { + resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] || backendType; + } + } + + return super.normalizePolymorphicType ? super.normalizePolymorphicType(...arguments) : resourceHash[typeKey]; + } + + /** + * Serialize polymorphic type back to the backend format. + * Converts Ember Data model names back to 'fleet-ops:*' strings. + */ + 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') { + const reverseMap = Object.fromEntries(Object.entries(MAINTENANCE_SUBJECT_TYPE_MAP).map(([k, v]) => [v, k])); + json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + } else if (key === 'default_assignee') { + const reverseMap = Object.fromEntries(Object.entries(FACILITATOR_TYPE_MAP).map(([k, v]) => [v, k])); + json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + } + } +} diff --git a/addon/serializers/maintenance.js b/addon/serializers/maintenance.js index 278a63b..895fd50 100644 --- a/addon/serializers/maintenance.js +++ b/addon/serializers/maintenance.js @@ -1,4 +1,71 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; -export default class MaintenanceSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {} +/** + * Type map for normalizing backend PolymorphicType strings to Ember Data model names. + * The backend uses Laravel's PolymorphicType cast which produces strings like 'fleet-ops:vehicle'. + */ +const MAINTENANCE_SUBJECT_TYPE_MAP = { + 'fleet-ops:vehicle': 'maintenance-subject-vehicle', + 'fleet-ops:equipment': 'maintenance-subject-equipment', +}; + +const FACILITATOR_TYPE_MAP = { + 'fleet-ops:driver': 'facilitator-contact', + 'fleet-ops:contact': 'facilitator-contact', + 'fleet-ops:vendor': 'facilitator-vendor', + 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', +}; + +export default class MaintenanceSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + get attrs() { + return { + maintainable: { embedded: 'always' }, + performed_by: { embedded: 'always' }, + work_order: { embedded: 'always' }, + custom_field_values: { embedded: 'always' }, + }; + } + + /** + * Normalize polymorphic type strings from the backend into Ember Data model names. + */ + normalizePolymorphicType(resourceHash, relationship) { + const key = relationship.key; + const typeKey = `${key}_type`; + const backendType = resourceHash[typeKey]; + + if (backendType) { + if (key === 'maintainable') { + resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] || backendType; + } else if (key === 'performed_by') { + resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] || backendType; + } + } + + return super.normalizePolymorphicType ? super.normalizePolymorphicType(...arguments) : resourceHash[typeKey]; + } + + /** + * Serialize polymorphic type back to the backend format. + */ + 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') { + const reverseMap = Object.fromEntries(Object.entries(MAINTENANCE_SUBJECT_TYPE_MAP).map(([k, v]) => [v, k])); + json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + } else if (key === 'performed_by') { + const reverseMap = Object.fromEntries(Object.entries(FACILITATOR_TYPE_MAP).map(([k, v]) => [v, k])); + json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + } + } +} diff --git a/addon/serializers/work-order.js b/addon/serializers/work-order.js index 6897d0f..68b4a6a 100644 --- a/addon/serializers/work-order.js +++ b/addon/serializers/work-order.js @@ -1,4 +1,69 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; -export default class WorkOrderSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {} +/** + * Type map for normalizing backend PolymorphicType strings to Ember Data model names. + * The backend uses Laravel's PolymorphicType cast which produces strings like 'fleet-ops:vehicle'. + */ +const MAINTENANCE_SUBJECT_TYPE_MAP = { + 'fleet-ops:vehicle': 'maintenance-subject-vehicle', + 'fleet-ops:equipment': 'maintenance-subject-equipment', +}; + +const FACILITATOR_TYPE_MAP = { + 'fleet-ops:driver': 'facilitator-contact', + 'fleet-ops:contact': 'facilitator-contact', + 'fleet-ops:vendor': 'facilitator-vendor', + 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', +}; + +export default class WorkOrderSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + get attrs() { + return { + target: { embedded: 'always' }, + assignee: { embedded: 'always' }, + }; + } + + /** + * Normalize polymorphic type strings from the backend into Ember Data model names. + */ + normalizePolymorphicType(resourceHash, relationship) { + const key = relationship.key; + const typeKey = `${key}_type`; + const backendType = resourceHash[typeKey]; + + if (backendType) { + if (key === 'target') { + resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] || backendType; + } else if (key === 'assignee') { + resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] || backendType; + } + } + + return super.normalizePolymorphicType ? super.normalizePolymorphicType(...arguments) : resourceHash[typeKey]; + } + + /** + * Serialize polymorphic type back to the backend format. + */ + 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') { + const reverseMap = Object.fromEntries(Object.entries(MAINTENANCE_SUBJECT_TYPE_MAP).map(([k, v]) => [v, k])); + json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + } else if (key === 'assignee') { + const reverseMap = Object.fromEntries(Object.entries(FACILITATOR_TYPE_MAP).map(([k, v]) => [v, k])); + json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + } + } +} 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'; From 0c2aa3e079b78271ec625c82a4f31710cd3b015d Mon Sep 17 00:00:00 2001 From: Ronald Richardson Date: Tue, 31 Mar 2026 21:59:10 -0400 Subject: [PATCH 07/14] fix(maintenance): correct PHP namespace type mapping and add _name fallback attrs --- addon/models/maintenance-schedule.js | 3 ++ addon/models/maintenance.js | 3 ++ addon/models/work-order.js | 3 ++ addon/serializers/maintenance-schedule.js | 65 +++++++++++++++-------- addon/serializers/maintenance.js | 52 +++++++++++------- addon/serializers/work-order.js | 55 ++++++++++++------- 6 files changed, 121 insertions(+), 60 deletions(-) diff --git a/addon/models/maintenance-schedule.js b/addon/models/maintenance-schedule.js index fe9b220..32238b1 100644 --- a/addon/models/maintenance-schedule.js +++ b/addon/models/maintenance-schedule.js @@ -11,6 +11,9 @@ export default class MaintenanceScheduleModel extends Model { /** @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; diff --git a/addon/models/maintenance.js b/addon/models/maintenance.js index d1da797..78edbe9 100644 --- a/addon/models/maintenance.js +++ b/addon/models/maintenance.js @@ -12,6 +12,9 @@ export default class MaintenanceModel extends Model { /** @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; /** @relationships */ @belongsTo('work-order', { async: false }) work_order; diff --git a/addon/models/work-order.js b/addon/models/work-order.js index 485d6dc..da1b2f5 100644 --- a/addon/models/work-order.js +++ b/addon/models/work-order.js @@ -12,6 +12,9 @@ export default class WorkOrderModel extends Model { /** @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; diff --git a/addon/serializers/maintenance-schedule.js b/addon/serializers/maintenance-schedule.js index 4cc317f..9db2d62 100644 --- a/addon/serializers/maintenance-schedule.js +++ b/addon/serializers/maintenance-schedule.js @@ -2,32 +2,55 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Type map for normalizing backend PolymorphicType strings to Ember Data model names. - * The backend uses Laravel's PolymorphicType cast which produces strings like 'fleet-ops:vehicle'. + * Maps the full PHP class names returned by the backend to the Ember Data + * model names used by the polymorphic @belongsTo relationships. + * + * The backend serialises subject_type / default_assignee_type as the full + * Laravel model class name (e.g. "Fleetbase\\FleetOps\\Models\\Vehicle"). + * Ember Data needs the dasherised model name (e.g. "maintenance-subject-vehicle"). */ const MAINTENANCE_SUBJECT_TYPE_MAP = { - 'fleet-ops:vehicle': 'maintenance-subject-vehicle', - 'fleet-ops:equipment': 'maintenance-subject-equipment', + 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', }; const FACILITATOR_TYPE_MAP = { - 'fleet-ops:driver': 'facilitator-contact', - 'fleet-ops:contact': 'facilitator-contact', - 'fleet-ops:vendor': 'facilitator-vendor', - 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', + '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', +}; + +const FACILITATOR_EMBER_TO_SHORTHAND = { + 'facilitator-vendor': 'fleet-ops:vendor', + 'facilitator-contact': 'fleet-ops:contact', + 'facilitator-integrated-vendor': 'fleet-ops:integrated-vendor', }; export default class MaintenanceScheduleSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + /** + * The subject and default_assignee relationships are NOT sideloaded in the + * server response — only the _type and _uuid foreign keys are returned. + * We therefore do NOT declare them as embedded; they will be resolved from + * the store if already cached, or loaded lazily on demand. + */ get attrs() { - return { - subject: { embedded: 'always' }, - default_assignee: { embedded: 'always' }, - }; + return {}; } /** - * Normalize polymorphic type strings from the backend into Ember Data model names. - * Called during deserialization for each polymorphic relationship. + * Normalise polymorphic type strings from the backend into Ember Data model + * names. Called during deserialisation for each polymorphic relationship. */ normalizePolymorphicType(resourceHash, relationship) { const key = relationship.key; @@ -36,9 +59,9 @@ export default class MaintenanceScheduleSerializer extends ApplicationSerializer if (backendType) { if (key === 'subject') { - resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] || backendType; + resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] ?? backendType; } else if (key === 'default_assignee') { - resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] || backendType; + resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] ?? backendType; } } @@ -46,8 +69,8 @@ export default class MaintenanceScheduleSerializer extends ApplicationSerializer } /** - * Serialize polymorphic type back to the backend format. - * Converts Ember Data model names back to 'fleet-ops:*' strings. + * 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; @@ -61,11 +84,9 @@ export default class MaintenanceScheduleSerializer extends ApplicationSerializer const modelName = belongsTo.modelName; if (key === 'subject') { - const reverseMap = Object.fromEntries(Object.entries(MAINTENANCE_SUBJECT_TYPE_MAP).map(([k, v]) => [v, k])); - json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + json[`${key}_type`] = SUBJECT_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; } else if (key === 'default_assignee') { - const reverseMap = Object.fromEntries(Object.entries(FACILITATOR_TYPE_MAP).map(([k, v]) => [v, k])); - json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + json[`${key}_type`] = FACILITATOR_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; } } } diff --git a/addon/serializers/maintenance.js b/addon/serializers/maintenance.js index 895fd50..ce3d792 100644 --- a/addon/serializers/maintenance.js +++ b/addon/serializers/maintenance.js @@ -2,33 +2,50 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Type map for normalizing backend PolymorphicType strings to Ember Data model names. - * The backend uses Laravel's PolymorphicType cast which produces strings like 'fleet-ops:vehicle'. + * Maps the full PHP class names returned by the backend to the Ember Data + * model names used by the polymorphic @belongsTo relationships. */ const MAINTENANCE_SUBJECT_TYPE_MAP = { - 'fleet-ops:vehicle': 'maintenance-subject-vehicle', - 'fleet-ops:equipment': 'maintenance-subject-equipment', + 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', }; const FACILITATOR_TYPE_MAP = { - 'fleet-ops:driver': 'facilitator-contact', - 'fleet-ops:contact': 'facilitator-contact', - 'fleet-ops:vendor': 'facilitator-vendor', - 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', + '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. + */ +const MAINTAINABLE_EMBER_TO_SHORTHAND = { + 'maintenance-subject-vehicle': 'fleet-ops:vehicle', + 'maintenance-subject-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', }; export default class MaintenanceSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + /** + * maintainable and performed_by are NOT sideloaded in the server response. + * work_order and custom_field_values are embedded and should remain so. + */ get attrs() { return { - maintainable: { embedded: 'always' }, - performed_by: { embedded: 'always' }, work_order: { embedded: 'always' }, custom_field_values: { embedded: 'always' }, }; } /** - * Normalize polymorphic type strings from the backend into Ember Data model names. + * Normalise polymorphic type strings from the backend into Ember Data model names. */ normalizePolymorphicType(resourceHash, relationship) { const key = relationship.key; @@ -37,9 +54,9 @@ export default class MaintenanceSerializer extends ApplicationSerializer.extend( if (backendType) { if (key === 'maintainable') { - resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] || backendType; + resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] ?? backendType; } else if (key === 'performed_by') { - resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] || backendType; + resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] ?? backendType; } } @@ -47,7 +64,8 @@ export default class MaintenanceSerializer extends ApplicationSerializer.extend( } /** - * Serialize polymorphic type back to the backend format. + * Serialise the polymorphic type back to the shorthand string the backend + * expects on create / update. */ serializePolymorphicType(snapshot, json, relationship) { const key = relationship.key; @@ -61,11 +79,9 @@ export default class MaintenanceSerializer extends ApplicationSerializer.extend( const modelName = belongsTo.modelName; if (key === 'maintainable') { - const reverseMap = Object.fromEntries(Object.entries(MAINTENANCE_SUBJECT_TYPE_MAP).map(([k, v]) => [v, k])); - json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + json[`${key}_type`] = MAINTAINABLE_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; } else if (key === 'performed_by') { - const reverseMap = Object.fromEntries(Object.entries(FACILITATOR_TYPE_MAP).map(([k, v]) => [v, k])); - json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + json[`${key}_type`] = PERFORMED_BY_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; } } } diff --git a/addon/serializers/work-order.js b/addon/serializers/work-order.js index 68b4a6a..a74612e 100644 --- a/addon/serializers/work-order.js +++ b/addon/serializers/work-order.js @@ -2,31 +2,47 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Type map for normalizing backend PolymorphicType strings to Ember Data model names. - * The backend uses Laravel's PolymorphicType cast which produces strings like 'fleet-ops:vehicle'. + * Maps the full PHP class names returned by the backend to the Ember Data + * model names used by the polymorphic @belongsTo relationships. */ const MAINTENANCE_SUBJECT_TYPE_MAP = { - 'fleet-ops:vehicle': 'maintenance-subject-vehicle', - 'fleet-ops:equipment': 'maintenance-subject-equipment', + 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', }; const FACILITATOR_TYPE_MAP = { - 'fleet-ops:driver': 'facilitator-contact', - 'fleet-ops:contact': 'facilitator-contact', - 'fleet-ops:vendor': 'facilitator-vendor', - 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', + '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. + */ +const TARGET_EMBER_TO_SHORTHAND = { + 'maintenance-subject-vehicle': 'fleet-ops:vehicle', + 'maintenance-subject-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', }; export default class WorkOrderSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + /** + * The target and assignee relationships are NOT sideloaded in the server + * response — only the _type and _uuid foreign keys are returned. + */ get attrs() { - return { - target: { embedded: 'always' }, - assignee: { embedded: 'always' }, - }; + return {}; } /** - * Normalize polymorphic type strings from the backend into Ember Data model names. + * Normalise polymorphic type strings from the backend into Ember Data model names. */ normalizePolymorphicType(resourceHash, relationship) { const key = relationship.key; @@ -35,9 +51,9 @@ export default class WorkOrderSerializer extends ApplicationSerializer.extend(Em if (backendType) { if (key === 'target') { - resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] || backendType; + resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] ?? backendType; } else if (key === 'assignee') { - resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] || backendType; + resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] ?? backendType; } } @@ -45,7 +61,8 @@ export default class WorkOrderSerializer extends ApplicationSerializer.extend(Em } /** - * Serialize polymorphic type back to the backend format. + * Serialise the polymorphic type back to the shorthand string the backend + * expects on create / update. */ serializePolymorphicType(snapshot, json, relationship) { const key = relationship.key; @@ -59,11 +76,9 @@ export default class WorkOrderSerializer extends ApplicationSerializer.extend(Em const modelName = belongsTo.modelName; if (key === 'target') { - const reverseMap = Object.fromEntries(Object.entries(MAINTENANCE_SUBJECT_TYPE_MAP).map(([k, v]) => [v, k])); - json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + json[`${key}_type`] = TARGET_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; } else if (key === 'assignee') { - const reverseMap = Object.fromEntries(Object.entries(FACILITATOR_TYPE_MAP).map(([k, v]) => [v, k])); - json[`${key}_type`] = reverseMap[modelName] || `fleet-ops:${modelName}`; + json[`${key}_type`] = ASSIGNEE_EMBER_TO_SHORTHAND[modelName] ?? `fleet-ops:${modelName}`; } } } From 50a4d02a57de331e3608f42ea8c6c5cb7ca9fc66 Mon Sep 17 00:00:00 2001 From: Ronald Richardson Date: Tue, 31 Mar 2026 22:29:30 -0400 Subject: [PATCH 08/14] feat(maintenance): re-enable embedded relationships now that resource transformers exist - maintenance-schedule: subject + default_assignee now embedded: always - work-order: target + assignee + custom_field_values now embedded: always - maintenance: maintainable + performed_by now embedded: always (work_order and custom_field_values were already embedded) The backend PHP resource transformers (MaintenanceSchedule, WorkOrder, Maintenance) eager-load these relationships via $with and embed them in every response, so the frontend serializers can safely declare them as embedded: always. --- addon/serializers/maintenance-schedule.js | 11 ++++++----- addon/serializers/maintenance.js | 6 ++++-- addon/serializers/work-order.js | 10 +++++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/addon/serializers/maintenance-schedule.js b/addon/serializers/maintenance-schedule.js index 9db2d62..0ee7aca 100644 --- a/addon/serializers/maintenance-schedule.js +++ b/addon/serializers/maintenance-schedule.js @@ -39,13 +39,14 @@ const FACILITATOR_EMBER_TO_SHORTHAND = { export default class MaintenanceScheduleSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { /** - * The subject and default_assignee relationships are NOT sideloaded in the - * server response — only the _type and _uuid foreign keys are returned. - * We therefore do NOT declare them as embedded; they will be resolved from - * the store if already cached, or loaded lazily on demand. + * The subject and default_assignee relationships are always embedded in the + * server response via the MaintenanceSchedule resource transformer. */ get attrs() { - return {}; + return { + subject: { embedded: 'always' }, + default_assignee: { embedded: 'always' }, + }; } /** diff --git a/addon/serializers/maintenance.js b/addon/serializers/maintenance.js index ce3d792..4e3d8e5 100644 --- a/addon/serializers/maintenance.js +++ b/addon/serializers/maintenance.js @@ -34,11 +34,13 @@ const PERFORMED_BY_EMBER_TO_SHORTHAND = { export default class MaintenanceSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { /** - * maintainable and performed_by are NOT sideloaded in the server response. - * work_order and custom_field_values are embedded and should remain so. + * 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' }, }; diff --git a/addon/serializers/work-order.js b/addon/serializers/work-order.js index a74612e..b3f59a6 100644 --- a/addon/serializers/work-order.js +++ b/addon/serializers/work-order.js @@ -34,11 +34,15 @@ const ASSIGNEE_EMBER_TO_SHORTHAND = { export default class WorkOrderSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { /** - * The target and assignee relationships are NOT sideloaded in the server - * response — only the _type and _uuid foreign keys are returned. + * The target and assignee relationships are always embedded in the server + * response via the WorkOrder resource transformer. */ get attrs() { - return {}; + return { + target: { embedded: 'always' }, + assignee: { embedded: 'always' }, + custom_field_values: { embedded: 'always' }, + }; } /** From 6f170b9673c843b5cd8eeaa693c301775117b603 Mon Sep 17 00:00:00 2001 From: Ronald Richardson Date: Tue, 31 Mar 2026 23:28:58 -0400 Subject: [PATCH 09/14] fix(maintenance): update serializer type maps to use shorthand from backend --- addon/serializers/maintenance-schedule.js | 28 ++++++++++++++--------- addon/serializers/maintenance.js | 17 +++++++------- addon/serializers/work-order.js | 17 +++++++------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/addon/serializers/maintenance-schedule.js b/addon/serializers/maintenance-schedule.js index 0ee7aca..4aec491 100644 --- a/addon/serializers/maintenance-schedule.js +++ b/addon/serializers/maintenance-schedule.js @@ -2,23 +2,25 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Maps the full PHP class names returned by the backend to the Ember Data - * model names used by the polymorphic @belongsTo relationships. + * Maps the shorthand type strings produced by Utils::toEmberResourceType() on the + * backend to the Ember Data model names used by the polymorphic @belongsTo + * relationships. * - * The backend serialises subject_type / default_assignee_type as the full - * Laravel model class name (e.g. "Fleetbase\\FleetOps\\Models\\Vehicle"). - * Ember Data needs the dasherised model name (e.g. "maintenance-subject-vehicle"). + * The backend now outputs e.g. "fleet-ops:vehicle" (not the raw PHP class name) + * for subject_type / default_assignee_type, and injects a `type` field of + * 'maintenance-subject' or 'facilitator' into every embedded relationship object + * so Ember Data can resolve the correct abstract model. */ const MAINTENANCE_SUBJECT_TYPE_MAP = { - 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', - 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', + 'fleet-ops:vehicle': 'maintenance-subject-vehicle', + 'fleet-ops:equipment': 'maintenance-subject-equipment', }; const FACILITATOR_TYPE_MAP = { - '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', + 'fleet-ops:vendor': 'facilitator-vendor', + 'fleet-ops:contact': 'facilitator-contact', + 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', + 'fleet-ops:driver': 'facilitator-contact', }; /** @@ -52,6 +54,10 @@ export default class MaintenanceScheduleSerializer extends ApplicationSerializer /** * Normalise polymorphic type strings from the backend into Ember Data model * names. Called during deserialisation for each polymorphic relationship. + * + * The backend injects a `type` field of 'maintenance-subject' or 'facilitator' + * into the embedded object, and the parent record carries the shorthand type + * (e.g. 'fleet-ops:vehicle') in subject_type / default_assignee_type. */ normalizePolymorphicType(resourceHash, relationship) { const key = relationship.key; diff --git a/addon/serializers/maintenance.js b/addon/serializers/maintenance.js index 4e3d8e5..51aa295 100644 --- a/addon/serializers/maintenance.js +++ b/addon/serializers/maintenance.js @@ -2,19 +2,20 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Maps the full PHP class names returned by the backend to the Ember Data - * model names used by the polymorphic @belongsTo relationships. + * Maps the shorthand type strings produced by Utils::toEmberResourceType() on the + * backend to the Ember Data model names used by the polymorphic @belongsTo + * relationships. */ const MAINTENANCE_SUBJECT_TYPE_MAP = { - 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', - 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', + 'fleet-ops:vehicle': 'maintenance-subject-vehicle', + 'fleet-ops:equipment': 'maintenance-subject-equipment', }; const FACILITATOR_TYPE_MAP = { - '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', + 'fleet-ops:vendor': 'facilitator-vendor', + 'fleet-ops:contact': 'facilitator-contact', + 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', + 'fleet-ops:driver': 'facilitator-contact', }; /** diff --git a/addon/serializers/work-order.js b/addon/serializers/work-order.js index b3f59a6..89449e3 100644 --- a/addon/serializers/work-order.js +++ b/addon/serializers/work-order.js @@ -2,19 +2,20 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Maps the full PHP class names returned by the backend to the Ember Data - * model names used by the polymorphic @belongsTo relationships. + * Maps the shorthand type strings produced by Utils::toEmberResourceType() on the + * backend to the Ember Data model names used by the polymorphic @belongsTo + * relationships. */ const MAINTENANCE_SUBJECT_TYPE_MAP = { - 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', - 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', + 'fleet-ops:vehicle': 'maintenance-subject-vehicle', + 'fleet-ops:equipment': 'maintenance-subject-equipment', }; const FACILITATOR_TYPE_MAP = { - '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', + 'fleet-ops:vendor': 'facilitator-vendor', + 'fleet-ops:contact': 'facilitator-contact', + 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', + 'fleet-ops:driver': 'facilitator-contact', }; /** From 0a43ce74366c8914692475ba31782686d59fb01d Mon Sep 17 00:00:00 2001 From: Ronald Richardson Date: Tue, 31 Mar 2026 23:37:03 -0400 Subject: [PATCH 10/14] revert(maintenance): restore PHP class name maps in normalizePolymorphicType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit incorrectly changed the normalizePolymorphicType maps from PHP class names to shorthand strings. This would break any model that does not go through the new resource transformers. The maps must stay as PHP class names since that is what the backend returns for *_type fields on the parent record. The backend resource transformers already handle the Ember Data type resolution problem correctly by injecting a 'type' field into the embedded object — the serializer maps do not need to change. --- addon/serializers/maintenance-schedule.js | 24 +++++++++++------------ addon/serializers/maintenance.js | 17 ++++++++-------- addon/serializers/work-order.js | 17 ++++++++-------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/addon/serializers/maintenance-schedule.js b/addon/serializers/maintenance-schedule.js index 4aec491..4e18aad 100644 --- a/addon/serializers/maintenance-schedule.js +++ b/addon/serializers/maintenance-schedule.js @@ -2,25 +2,23 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Maps the shorthand type strings produced by Utils::toEmberResourceType() on the - * backend to the Ember Data model names used by the polymorphic @belongsTo - * relationships. + * Maps the full PHP class names returned by the backend to the Ember Data + * model names used by the polymorphic @belongsTo relationships. * - * The backend now outputs e.g. "fleet-ops:vehicle" (not the raw PHP class name) - * for subject_type / default_assignee_type, and injects a `type` field of - * 'maintenance-subject' or 'facilitator' into every embedded relationship object - * so Ember Data can resolve the correct abstract model. + * The backend serialises subject_type / default_assignee_type as the full + * Laravel model class name (e.g. "Fleetbase\\FleetOps\\Models\\Vehicle"). + * Ember Data needs the dasherised model name (e.g. "maintenance-subject-vehicle"). */ const MAINTENANCE_SUBJECT_TYPE_MAP = { - 'fleet-ops:vehicle': 'maintenance-subject-vehicle', - 'fleet-ops:equipment': 'maintenance-subject-equipment', + 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', }; const FACILITATOR_TYPE_MAP = { - 'fleet-ops:vendor': 'facilitator-vendor', - 'fleet-ops:contact': 'facilitator-contact', - 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', - 'fleet-ops:driver': 'facilitator-contact', + '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', }; /** diff --git a/addon/serializers/maintenance.js b/addon/serializers/maintenance.js index 51aa295..4e3d8e5 100644 --- a/addon/serializers/maintenance.js +++ b/addon/serializers/maintenance.js @@ -2,20 +2,19 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Maps the shorthand type strings produced by Utils::toEmberResourceType() on the - * backend to the Ember Data model names used by the polymorphic @belongsTo - * relationships. + * Maps the full PHP class names returned by the backend to the Ember Data + * model names used by the polymorphic @belongsTo relationships. */ const MAINTENANCE_SUBJECT_TYPE_MAP = { - 'fleet-ops:vehicle': 'maintenance-subject-vehicle', - 'fleet-ops:equipment': 'maintenance-subject-equipment', + 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', }; const FACILITATOR_TYPE_MAP = { - 'fleet-ops:vendor': 'facilitator-vendor', - 'fleet-ops:contact': 'facilitator-contact', - 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', - 'fleet-ops:driver': 'facilitator-contact', + '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', }; /** diff --git a/addon/serializers/work-order.js b/addon/serializers/work-order.js index 89449e3..b3f59a6 100644 --- a/addon/serializers/work-order.js +++ b/addon/serializers/work-order.js @@ -2,20 +2,19 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Maps the shorthand type strings produced by Utils::toEmberResourceType() on the - * backend to the Ember Data model names used by the polymorphic @belongsTo - * relationships. + * Maps the full PHP class names returned by the backend to the Ember Data + * model names used by the polymorphic @belongsTo relationships. */ const MAINTENANCE_SUBJECT_TYPE_MAP = { - 'fleet-ops:vehicle': 'maintenance-subject-vehicle', - 'fleet-ops:equipment': 'maintenance-subject-equipment', + 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + 'Fleetbase\\FleetOps\\Models\\Equipment': 'maintenance-subject-equipment', }; const FACILITATOR_TYPE_MAP = { - 'fleet-ops:vendor': 'facilitator-vendor', - 'fleet-ops:contact': 'facilitator-contact', - 'fleet-ops:integrated-vendor': 'facilitator-integrated-vendor', - 'fleet-ops:driver': 'facilitator-contact', + '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', }; /** From fbe793458a9f7bae202359261784be3c723f9a11 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 1 Apr 2026 03:25:56 -0400 Subject: [PATCH 11/14] fix: change monetary attributes to @attr('string') type on maintenance models MoneyInput component emits cents as integers, and the backend Money cast stores/retrieves integer cent values. Using @attr('number') caused JS number coercion issues with large cent values. Using @attr('string') passes the raw integer string through Ember Data without any transformation, which is correct for MoneyInput's @value binding. Affected models and attributes: - equipment: purchase_price - part: unit_cost, msrp - maintenance: labor_cost, parts_cost, tax, total_cost - work-order: estimated_cost, approved_budget, actual_cost --- addon/models/equipment.js | 3 ++- addon/models/maintenance.js | 9 +++++---- addon/models/part.js | 5 +++-- addon/models/work-order.js | 7 +++++++ 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/addon/models/equipment.js b/addon/models/equipment.js index bd0e49c..df74e04 100644 --- a/addon/models/equipment.js +++ b/addon/models/equipment.js @@ -25,7 +25,8 @@ 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; @attr('string') warranty_name; diff --git a/addon/models/maintenance.js b/addon/models/maintenance.js index 78edbe9..9cf7cf9 100644 --- a/addon/models/maintenance.js +++ b/addon/models/maintenance.js @@ -29,10 +29,11 @@ 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; diff --git a/addon/models/part.js b/addon/models/part.js index 25b5401..4b1ca32 100644 --- a/addon/models/part.js +++ b/addon/models/part.js @@ -26,8 +26,9 @@ 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; diff --git a/addon/models/work-order.js b/addon/models/work-order.js index da1b2f5..b494f56 100644 --- a/addon/models/work-order.js +++ b/addon/models/work-order.js @@ -23,6 +23,13 @@ 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; From d5bd42ccf6fb592e90038e8335737a0d05c6405d Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 1 Apr 2026 03:51:34 -0400 Subject: [PATCH 12/14] fix: remove slug attr from work-order Ember model (no DB column) --- addon/models/work-order.js | 1 - 1 file changed, 1 deletion(-) diff --git a/addon/models/work-order.js b/addon/models/work-order.js index b494f56..cb013c0 100644 --- a/addon/models/work-order.js +++ b/addon/models/work-order.js @@ -31,7 +31,6 @@ export default class WorkOrderModel extends Model { @attr('string') cost_center; @attr('string') budget_code; @attr('raw') meta; - @attr('string') slug; /** @dates */ @attr('date') opened_at; From 70581a0c8d53015fcefdb089899f4943f5f384ea Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 1 Apr 2026 06:20:42 -0400 Subject: [PATCH 13/14] feat: add all missing server-side fields to maintenance-related Ember Data models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited every maintenance-related server-side resource (fillable + appends) against the corresponding fleetops-data Ember Data model and added all fields that were present in the API response but absent from the model definition. Equipment: + uuid + equipped_to_name (server appended) + is_equipped (server appended) + age_in_days (server appended — computed from purchased_at) + depreciated_value (server appended — straight-line depreciation) Part: + uuid + total_value (server appended — quantity_on_hand × unit_cost) + is_in_stock (server appended) + is_low_stock (server appended) + asset_name (server appended) WorkOrder: + is_overdue (server appended) + days_until_due (server appended) + completion_percentage (server appended) + estimated_duration (server appended) (estimated_cost, approved_budget, actual_cost, currency, cost_breakdown, cost_center, budget_code were already present from a prior commit) Maintenance: + work_order_subject (server convenience field) + duration_hours (server appended — diff of started_at/completed_at) + is_overdue (server appended) + days_until_due (server appended) + cost_breakdown (server appended) Asset: + uuid + current_location (server appended) + is_online (server appended) + last_maintenance (server appended) + next_maintenance_due (server appended) Warranty: + uuid + subject_name (server appended) + is_active (server appended) + is_expired (server appended) + days_remaining (server appended) + coverage_summary (server appended) + status (server appended) Device: + uuid + warranty_name (server appended) + telematic_name (server appended) + is_online (server appended) + attached_to_name (server appended) + connection_status (server appended) Sensor: + uuid Reorganised: moved photo_url, is_active, threshold_status under a clear "server-computed" section (they were already declared but mixed into attributes) MaintenanceSchedule: no changes needed — subject_name and default_assignee_name were already present. --- addon/models/asset.js | 6 ++++++ addon/models/device.js | 9 +++++++++ addon/models/equipment.js | 6 ++++++ addon/models/maintenance.js | 7 +++++++ addon/models/part.js | 6 ++++++ addon/models/sensor.js | 9 ++++++--- addon/models/warranty.js | 9 +++++++++ addon/models/work-order.js | 6 ++++++ 8 files changed, 55 insertions(+), 3 deletions(-) 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/equipment.js b/addon/models/equipment.js index df74e04..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; @@ -29,8 +30,13 @@ export default class EquipmentModel extends Model { @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.js b/addon/models/maintenance.js index 9cf7cf9..8bbaf83 100644 --- a/addon/models/maintenance.js +++ b/addon/models/maintenance.js @@ -15,6 +15,7 @@ export default class MaintenanceModel extends Model { /** @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; @@ -38,6 +39,12 @@ export default class MaintenanceModel extends Model { @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 4b1ca32..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; @@ -34,9 +35,14 @@ export default class PartModel extends Model { @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 cb013c0..c60c454 100644 --- a/addon/models/work-order.js +++ b/addon/models/work-order.js @@ -32,6 +32,12 @@ export default class WorkOrderModel extends Model { @attr('string') budget_code; @attr('raw') meta; + /** @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; @attr('date') due_at; From 2012f3528d300a3fe95759a89e5f89ce36c3ab57 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Thu, 2 Apr 2026 00:35:30 -0400 Subject: [PATCH 14/14] fix(serializers): expand type maps and prefer injected subject_type/facilitator_type All three maintenance serializers (work-order, maintenance, maintenance-schedule) previously only mapped full PHP class names in MAINTENANCE_SUBJECT_TYPE_MAP and FACILITATOR_TYPE_MAP. The fixed backend resources now inject bare slug strings (e.g. 'facilitator-vendor', 'maintenance-subject-vehicle') via subject_type and facilitator_type fields on the embedded object, so the old maps never matched. Changes: - Expand all type maps to cover: bare slug, fleet-ops: short-form, bare model name, and full PHP class name - so any variant the backend ever sends is handled. - normalizePolymorphicType now reads the injected subject_type / facilitator_type field first (set by the fixed PHP resource), falling back to the raw _type field. - serializePolymorphicType EMBER_TO_SHORTHAND maps extended to cover raw model names (vehicle, equipment, vendor, contact, driver) so saving works even when the record was set directly from a non-subtyped model (e.g. target=@vehicle). Affected serializers: work-order, maintenance, maintenance-schedule. --- addon/serializers/maintenance-schedule.js | 94 +++++++++++++++++------ addon/serializers/maintenance.js | 88 +++++++++++++++++---- addon/serializers/work-order.js | 87 +++++++++++++++++---- 3 files changed, 214 insertions(+), 55 deletions(-) diff --git a/addon/serializers/maintenance-schedule.js b/addon/serializers/maintenance-schedule.js index 4e18aad..9225d0d 100644 --- a/addon/serializers/maintenance-schedule.js +++ b/addon/serializers/maintenance-schedule.js @@ -2,23 +2,55 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Maps the full PHP class names returned by the backend to the Ember Data - * model names used by the polymorphic @belongsTo relationships. + * 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 backend serialises subject_type / default_assignee_type as the full - * Laravel model class name (e.g. "Fleetbase\\FleetOps\\Models\\Vehicle"). - * Ember Data needs the dasherised model name (e.g. "maintenance-subject-vehicle"). + * 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 = { - 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + // 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 = { - 'Fleetbase\\FleetOps\\Models\\Vendor': 'facilitator-vendor', - 'Fleetbase\\FleetOps\\Models\\Contact': 'facilitator-contact', + // 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', + 'Fleetbase\\FleetOps\\Models\\Driver': 'facilitator-contact', }; /** @@ -27,14 +59,22 @@ const FACILITATOR_TYPE_MAP = { * to the correct PHP namespace internally. */ const SUBJECT_EMBER_TO_SHORTHAND = { - 'maintenance-subject-vehicle': 'fleet-ops:vehicle', + '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-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) { @@ -50,23 +90,31 @@ export default class MaintenanceScheduleSerializer extends ApplicationSerializer } /** - * Normalise polymorphic type strings from the backend into Ember Data model - * names. Called during deserialisation for each polymorphic relationship. + * 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. * - * The backend injects a `type` field of 'maintenance-subject' or 'facilitator' - * into the embedded object, and the parent record carries the shorthand type - * (e.g. 'fleet-ops:vehicle') in subject_type / default_assignee_type. + * 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`; - const backendType = resourceHash[typeKey]; - if (backendType) { - if (key === 'subject') { - resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] ?? backendType; - } else if (key === 'default_assignee') { - resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] ?? backendType; + 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; } } diff --git a/addon/serializers/maintenance.js b/addon/serializers/maintenance.js index 4e3d8e5..06649a5 100644 --- a/addon/serializers/maintenance.js +++ b/addon/serializers/maintenance.js @@ -2,34 +2,79 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Maps the full PHP class names returned by the backend to the Ember Data - * model names used by the polymorphic @belongsTo relationships. + * 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 = { - 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + // 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 = { - 'Fleetbase\\FleetOps\\Models\\Vendor': 'facilitator-vendor', - 'Fleetbase\\FleetOps\\Models\\Contact': 'facilitator-contact', + // 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', + 'Fleetbase\\FleetOps\\Models\\Driver': 'facilitator-contact', }; /** * Maps the Ember Data model name back to the shorthand type string the - * backend expects on create / update. + * 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-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-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) { @@ -48,17 +93,28 @@ export default class MaintenanceSerializer extends ApplicationSerializer.extend( /** * 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`; - const backendType = resourceHash[typeKey]; - if (backendType) { - if (key === 'maintainable') { - resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] ?? backendType; - } else if (key === 'performed_by') { - resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] ?? backendType; + 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; } } diff --git a/addon/serializers/work-order.js b/addon/serializers/work-order.js index b3f59a6..22d6274 100644 --- a/addon/serializers/work-order.js +++ b/addon/serializers/work-order.js @@ -2,34 +2,78 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; /** - * Maps the full PHP class names returned by the backend to the Ember Data - * model names used by the polymorphic @belongsTo relationships. + * 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 = { - 'Fleetbase\\FleetOps\\Models\\Vehicle': 'maintenance-subject-vehicle', + // 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 = { - 'Fleetbase\\FleetOps\\Models\\Vendor': 'facilitator-vendor', - 'Fleetbase\\FleetOps\\Models\\Contact': 'facilitator-contact', + // 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', + 'Fleetbase\\FleetOps\\Models\\Driver': 'facilitator-contact', }; /** * Maps the Ember Data model name back to the shorthand type string the - * backend expects on create / update. + * 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-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-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) { @@ -47,17 +91,28 @@ export default class WorkOrderSerializer extends ApplicationSerializer.extend(Em /** * 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`; - const backendType = resourceHash[typeKey]; - if (backendType) { - if (key === 'target') { - resourceHash[typeKey] = MAINTENANCE_SUBJECT_TYPE_MAP[backendType] ?? backendType; - } else if (key === 'assignee') { - resourceHash[typeKey] = FACILITATOR_TYPE_MAP[backendType] ?? backendType; + 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; } }