diff --git a/addon/adapters/manifest-stop.js b/addon/adapters/manifest-stop.js new file mode 100644 index 0000000..cef55a5 --- /dev/null +++ b/addon/adapters/manifest-stop.js @@ -0,0 +1,11 @@ +import ApplicationAdapter from '@fleetbase/ember-core/adapters/application'; + +export default class ManifestStopAdapter extends ApplicationAdapter { + urlForFindRecord(id) { + return `${this.host}/${this.namespace}/fleet-ops/v1/manifest-stops/${id}`; + } + + urlForUpdateRecord(id) { + return `${this.host}/${this.namespace}/fleet-ops/v1/manifest-stops/${id}`; + } +} diff --git a/addon/adapters/manifest.js b/addon/adapters/manifest.js new file mode 100644 index 0000000..492b3ee --- /dev/null +++ b/addon/adapters/manifest.js @@ -0,0 +1,11 @@ +import ApplicationAdapter from '@fleetbase/ember-core/adapters/application'; + +export default class ManifestAdapter extends ApplicationAdapter { + urlForQuery() { + return `${this.host}/${this.namespace}/fleet-ops/v1/manifests`; + } + + urlForFindRecord(id) { + return `${this.host}/${this.namespace}/fleet-ops/v1/manifests/${id}`; + } +} diff --git a/addon/models/asset.js b/addon/models/asset.js index 6596ce0..8387795 100644 --- a/addon/models/asset.js +++ b/addon/models/asset.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class AssetModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') category_uuid; @@ -63,11 +64,16 @@ export default class AssetModel extends Model { @attr('raw') attributes; @attr('string') notes; @attr('string') slug; + /** @server-computed (read-only appended attributes) */ @attr('string') photo_url; @attr('string') display_name; @attr('string') category_name; @attr('string') vendor_name; @attr('string') warranty_name; + @attr('string') current_location; + @attr('boolean') is_online; + @attr('date') last_maintenance; + @attr('date') next_maintenance_due; /** @dates */ @attr('date') deleted_at; diff --git a/addon/models/device.js b/addon/models/device.js index 2c9d4c5..7b93af3 100644 --- a/addon/models/device.js +++ b/addon/models/device.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class DeviceModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') telematic_uuid; @@ -30,7 +31,15 @@ export default class DeviceModel extends Model { @attr('string') imsi; @attr('string') firmware_version; @attr('string') provider; + + /** @server-computed (read-only appended attributes) */ @attr('string') photo_url; + @attr('string') warranty_name; + @attr('string') telematic_name; + @attr('boolean') is_online; + @attr('string') attached_to_name; + @attr('string') connection_status; + @attr('string') manufacturer; @attr('string') serial_number; @attr('point') last_position; diff --git a/addon/models/driver.js b/addon/models/driver.js index a7b17aa..5acb245 100644 --- a/addon/models/driver.js +++ b/addon/models/driver.js @@ -31,6 +31,11 @@ export default class DriverModel extends Model { @belongsTo('vendor', { async: true }) vendor; @hasMany('custom-field-value', { async: false }) custom_field_values; + /** @scheduling-relationships */ + @hasMany('schedule', { async: true }) schedules; + @hasMany('schedule-item', { async: true }) schedule_items; + @hasMany('schedule-availability', { async: true }) availabilities; + /** @attributes */ @attr('string') name; @attr('string') phone; diff --git a/addon/models/equipment.js b/addon/models/equipment.js index bd0e49c..a897ade 100644 --- a/addon/models/equipment.js +++ b/addon/models/equipment.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class EquipmentModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') warranty_uuid; @@ -25,11 +26,17 @@ export default class EquipmentModel extends Model { @attr('string') serial_number; @attr('string') manufacturer; @attr('string') model; - @attr('number') purchase_price; + @attr('string') purchase_price; + @attr('string') currency; @attr('raw') meta; @attr('string') slug; + /** @server-computed (read-only appended attributes) */ @attr('string') warranty_name; @attr('string') photo_url; + @attr('string') equipped_to_name; + @attr('boolean') is_equipped; + @attr('number') age_in_days; + @attr('string') depreciated_value; /** @dates */ @attr('date') purchased_at; diff --git a/addon/models/maintenance-schedule.js b/addon/models/maintenance-schedule.js new file mode 100644 index 0000000..818abd8 --- /dev/null +++ b/addon/models/maintenance-schedule.js @@ -0,0 +1,133 @@ +import Model, { attr, belongsTo } from '@ember-data/model'; +import { computed } from '@ember/object'; +import { format as formatDate, isValid as isValidDate, formatDistanceToNow } from 'date-fns'; + +export default class MaintenanceScheduleModel extends Model { + /** @ids */ + @attr('string') uuid; + @attr('string') public_id; + @attr('string') company_uuid; + + /** @polymorphic relationships */ + @belongsTo('maintenance-subject', { polymorphic: true, async: false }) subject; + @belongsTo('facilitator', { polymorphic: true, async: false }) default_assignee; + /** @computed names — server-side convenience fields (read-only) */ + @attr('string') subject_name; + @attr('string') default_assignee_name; + + /** @attributes */ + @attr('string') code; + @attr('string') title; + @attr('string') description; + @attr('string') name; + @attr('string') type; + @attr('string') status; + @attr('string') interval_method; + + /** @interval — time-based */ + @attr('string') interval_type; + @attr('number') interval_value; + @attr('string') interval_unit; + + /** @interval — distance / engine-hours */ + @attr('number') interval_distance; + @attr('number') interval_engine_hours; + + /** @baseline readings */ + @attr('number') last_service_odometer; + @attr('number') last_service_engine_hours; + @attr('date') last_service_date; + + /** @next-due thresholds */ + @attr('date') next_due_date; + @attr('number') next_due_odometer; + @attr('number') next_due_engine_hours; + + /** @work-order defaults */ + @attr('string') default_priority; + + @attr('string') instructions; + @attr('raw') meta; + @attr('string') slug; + + /** @reminders — array of integer day offsets, e.g. [15, 7, 3] */ + @attr('raw') reminder_offsets; + + /** @dates */ + @attr('date') last_triggered_at; + @attr('date') deleted_at; + @attr('date') created_at; + @attr('date') updated_at; + + /** @computed */ + @computed('status') get isActive() { + return this.status === 'active'; + } + + @computed('status') get isPaused() { + return this.status === 'paused'; + } + + @computed('next_due_date') get nextDueAt() { + if (!isValidDate(this.next_due_date)) { + return null; + } + return formatDate(this.next_due_date, 'yyyy-MM-dd HH:mm'); + } + + @computed('next_due_date') get nextDueAtShort() { + if (!isValidDate(this.next_due_date)) { + return null; + } + return formatDate(this.next_due_date, 'dd, MMM yyyy'); + } + + @computed('next_due_date') get nextDueAgo() { + if (!isValidDate(this.next_due_date)) { + return null; + } + return formatDistanceToNow(this.next_due_date, { addSuffix: true }); + } + + @computed('updated_at') get updatedAgo() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDistanceToNow(this.updated_at); + } + + @computed('updated_at') get updatedAt() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDate(this.updated_at, 'yyyy-MM-dd HH:mm'); + } + + @computed('updated_at') get updatedAtShort() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDate(this.updated_at, 'dd, MMM'); + } + + @computed('created_at') get createdAgo() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDistanceToNow(this.created_at); + } + + @computed('created_at') get createdAt() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDate(this.created_at, 'yyyy-MM-dd HH:mm'); + } + + @computed('created_at') get createdAtShort() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDate(this.created_at, 'dd, MMM'); + } +} diff --git a/addon/models/maintenance-subject-equipment.js b/addon/models/maintenance-subject-equipment.js new file mode 100644 index 0000000..d036a1e --- /dev/null +++ b/addon/models/maintenance-subject-equipment.js @@ -0,0 +1,26 @@ +import MaintenanceSubjectModel from './maintenance-subject'; +import { attr } from '@ember-data/model'; + +/** + * Concrete polymorphic model for Equipment acting as a maintenance subject. + * Resolved when the backend sends subject_type / target_type / maintainable_type = 'fleet-ops:equipment'. + */ +export default class MaintenanceSubjectEquipmentModel extends MaintenanceSubjectModel { + /** @ids */ + @attr('string') warranty_uuid; + @attr('string') photo_uuid; + @attr('string') equipable_type; + @attr('string') equipable_uuid; + + /** @attributes */ + @attr('string') code; + @attr('string') serial_number; + @attr('string') manufacturer; + @attr('string') model; + @attr('number') purchase_price; + @attr('string') warranty_name; + @attr('raw') meta; + + /** @dates */ + @attr('date') purchased_at; +} diff --git a/addon/models/maintenance-subject-vehicle.js b/addon/models/maintenance-subject-vehicle.js new file mode 100644 index 0000000..088526b --- /dev/null +++ b/addon/models/maintenance-subject-vehicle.js @@ -0,0 +1,44 @@ +import MaintenanceSubjectModel from './maintenance-subject'; +import { attr } from '@ember-data/model'; +import { get } from '@ember/object'; +import config from 'ember-get-config'; + +/** + * Concrete polymorphic model for a Vehicle acting as a maintenance subject. + * Resolved when the backend sends subject_type / target_type / maintainable_type = 'fleet-ops:vehicle'. + */ +export default class MaintenanceSubjectVehicleModel extends MaintenanceSubjectModel { + /** @ids */ + @attr('string') internal_id; + @attr('string') photo_uuid; + @attr('string') vendor_uuid; + @attr('string') category_uuid; + @attr('string') warranty_uuid; + @attr('string') telematic_uuid; + + /** @attributes */ + @attr('string', { + defaultValue: get(config, 'defaultValues.vehicleImage'), + }) + photo_url; + + @attr('string') make; + @attr('string') model; + @attr('string') year; + @attr('string') trim; + @attr('string') plate_number; + @attr('string') vin; + @attr('string') driver_name; + @attr('string') vendor_name; + @attr('string') display_name; + @attr('string', { + defaultValue: get(config, 'defaultValues.vehicleAvatar'), + }) + avatar_url; + @attr('string') avatar_value; + @attr('string') color; + @attr('string') country; + @attr('number') odometer; + @attr('number') engine_hours; + @attr('raw') meta; +} diff --git a/addon/models/maintenance-subject.js b/addon/models/maintenance-subject.js new file mode 100644 index 0000000..efaff85 --- /dev/null +++ b/addon/models/maintenance-subject.js @@ -0,0 +1,78 @@ +import Model, { attr } from '@ember-data/model'; +import { computed } from '@ember/object'; +import { format as formatDate, isValid as isValidDate, formatDistanceToNow } from 'date-fns'; + +/** + * Abstract base model for polymorphic maintenance subjects. + * Concrete types: maintenance-subject-vehicle, maintenance-subject-equipment + * + * The backend stores the type as a PolymorphicType cast string, e.g.: + * 'fleet-ops:vehicle' -> maintenance-subject-vehicle + * 'fleet-ops:equipment' -> maintenance-subject-equipment + */ +export default class MaintenanceSubjectModel extends Model { + /** @ids */ + @attr('string') uuid; + @attr('string') public_id; + @attr('string') company_uuid; + + /** @attributes */ + @attr('string') name; + @attr('string') display_name; + @attr('string') type; + @attr('string') status; + @attr('string') photo_url; + @attr('string') slug; + + /** @dates */ + @attr('date') deleted_at; + @attr('date') created_at; + @attr('date') updated_at; + + /** @computed */ + @computed('name', 'display_name', 'public_id') get displayName() { + return this.display_name || this.name || this.public_id; + } + + @computed('updated_at') get updatedAgo() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDistanceToNow(this.updated_at); + } + + @computed('updated_at') get updatedAt() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDate(this.updated_at, 'yyyy-MM-dd HH:mm'); + } + + @computed('updated_at') get updatedAtShort() { + if (!isValidDate(this.updated_at)) { + return null; + } + return formatDate(this.updated_at, 'dd, MMM'); + } + + @computed('created_at') get createdAgo() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDistanceToNow(this.created_at); + } + + @computed('created_at') get createdAt() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDate(this.created_at, 'yyyy-MM-dd HH:mm'); + } + + @computed('created_at') get createdAtShort() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDate(this.created_at, 'dd, MMM'); + } +} diff --git a/addon/models/maintenance.js b/addon/models/maintenance.js index cc665ac..8bbaf83 100644 --- a/addon/models/maintenance.js +++ b/addon/models/maintenance.js @@ -4,13 +4,18 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class MaintenanceModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') work_order_uuid; - @attr('string') maintainable_type; - @attr('string') maintainable_uuid; - @attr('string') performed_by_type; - @attr('string') performed_by_uuid; + + /** @polymorphic relationships */ + @belongsTo('maintenance-subject', { polymorphic: true, async: false }) maintainable; + @belongsTo('facilitator', { polymorphic: true, async: false }) performed_by; + /** @computed names — server-side convenience fields (read-only) */ + @attr('string') maintainable_name; + @attr('string') performed_by_name; + @attr('string') work_order_subject; /** @relationships */ @belongsTo('work-order', { async: false }) work_order; @@ -25,14 +30,21 @@ export default class MaintenanceModel extends Model { @attr('string') summary; @attr('string') notes; @attr('raw') line_items; - @attr('number') labor_cost; - @attr('number') parts_cost; - @attr('number') tax; - @attr('number') total_cost; + @attr('string') labor_cost; + @attr('string') parts_cost; + @attr('string') tax; + @attr('string') total_cost; + @attr('string') currency; @attr('raw') attachments; @attr('raw') meta; @attr('string') slug; + /** @server-computed (read-only appended attributes) */ + @attr('number') duration_hours; + @attr('boolean') is_overdue; + @attr('number') days_until_due; + @attr('raw') cost_breakdown; + /** @dates */ @attr('date') scheduled_at; @attr('date') started_at; diff --git a/addon/models/manifest-stop.js b/addon/models/manifest-stop.js new file mode 100644 index 0000000..9bd64ef --- /dev/null +++ b/addon/models/manifest-stop.js @@ -0,0 +1,95 @@ +import Model, { attr, belongsTo } from '@ember-data/model'; +import { computed } from '@ember/object'; +import { format as formatDate, isValid as isValidDate } from 'date-fns'; + +/** + * ManifestStop model + * + * Represents a single physical stop within a Manifest. Each stop corresponds + * to one Order (or one waypoint within a multi-waypoint order) and carries + * a direct FK to the Place it represents for fast map rendering and geofence + * arrival detection in the Navigator app. + * + * Status lifecycle: pending → arrived → completed | skipped + */ +export default class ManifestStopModel extends Model { + /** @ids */ + @attr('string') public_id; + @attr('string') manifest_uuid; + @attr('string') order_uuid; + @attr('string') place_uuid; + @attr('string') waypoint_uuid; + + /** @relationships */ + @belongsTo('manifest', { async: false, inverse: 'stops' }) manifest; + @belongsTo('order', { async: false }) order; + @belongsTo('place', { async: false }) place; + + /** @attributes */ + @attr('number') sequence; + @attr('string') status; + @attr('string') notes; + + /** @timing */ + @attr('date') estimated_arrival; + @attr('date') actual_arrival; + + /** @distance from previous stop (metres) */ + @attr('number') distance_from_prev_m; + + /** @meta */ + @attr('raw') meta; + + /** @dates */ + @attr('date') created_at; + @attr('date') updated_at; + + /** @computed */ + @computed('status') get isPending() { + return this.status === 'pending'; + } + + @computed('status') get isArrived() { + return this.status === 'arrived'; + } + + @computed('status') get isCompleted() { + return this.status === 'completed'; + } + + @computed('status') get isSkipped() { + return this.status === 'skipped'; + } + + @computed('status') get statusLabel() { + const labels = { + pending: 'Pending', + arrived: 'Arrived', + completed: 'Completed', + skipped: 'Skipped', + }; + return labels[this.status] ?? this.status; + } + + @computed('estimated_arrival') get estimatedArrivalFormatted() { + if (!isValidDate(this.estimated_arrival)) { + return null; + } + return formatDate(this.estimated_arrival, 'HH:mm'); + } + + @computed('actual_arrival') get actualArrivalFormatted() { + if (!isValidDate(this.actual_arrival)) { + return null; + } + return formatDate(this.actual_arrival, 'HH:mm'); + } + + @computed('distance_from_prev_m') get distanceFromPrevKm() { + return this.distance_from_prev_m ? (this.distance_from_prev_m / 1000).toFixed(1) : null; + } + + @computed('sequence') get stopLabel() { + return `Stop ${this.sequence}`; + } +} diff --git a/addon/models/manifest.js b/addon/models/manifest.js new file mode 100644 index 0000000..ac47557 --- /dev/null +++ b/addon/models/manifest.js @@ -0,0 +1,139 @@ +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { computed } from '@ember/object'; +import { format as formatDate, isValid as isValidDate, formatDistanceToNow } from 'date-fns'; + +/** + * Manifest model + * + * Represents a committed delivery plan for a specific vehicle and (optionally) + * driver. Generated by the Orchestrator commit step. Contains an ordered list + * of ManifestStops representing the physical locations to visit in sequence. + * + * Status lifecycle: draft → active → in_progress → completed | cancelled + */ +export default class ManifestModel extends Model { + /** @ids */ + @attr('string') public_id; + @attr('string') internal_id; + @attr('string') company_uuid; + @attr('string') driver_uuid; + @attr('string') vehicle_uuid; + + /** @relationships */ + @belongsTo('driver', { async: false }) driver; + @belongsTo('vehicle', { async: false }) vehicle; + @hasMany('manifest-stop', { async: false }) stops; + + /** @attributes */ + @attr('string') status; + @attr('string') notes; + + /** @computed/appended by backend */ + @attr('string') driver_name; + @attr('string') vehicle_name; + @attr('number') stop_count; + @attr('number') completed_stops; + @attr('number') pending_stops; + + /** @totals from VROOM */ + @attr('number') total_distance_m; + @attr('number') total_duration_s; + + /** @meta */ + @attr('raw') meta; + + /** @dates */ + @attr('date') scheduled_date; + @attr('date') started_at; + @attr('date') completed_at; + @attr('date') deleted_at; + @attr('date') created_at; + @attr('date') updated_at; + + /** @computed */ + @computed('status') get isDraft() { + return this.status === 'draft'; + } + + @computed('status') get isActive() { + return this.status === 'active'; + } + + @computed('status') get isInProgress() { + return this.status === 'in_progress'; + } + + @computed('status') get isCompleted() { + return this.status === 'completed'; + } + + @computed('status') get isCancelled() { + return this.status === 'cancelled'; + } + + @computed('status') get statusLabel() { + const labels = { + draft: 'Draft', + active: 'Active', + in_progress: 'In Progress', + completed: 'Completed', + cancelled: 'Cancelled', + }; + return labels[this.status] ?? this.status; + } + + @computed('stop_count', 'completed_stops') get progressPercent() { + if (!this.stop_count || this.stop_count === 0) { + return 0; + } + return Math.round(((this.completed_stops ?? 0) / this.stop_count) * 100); + } + + @computed('total_distance_m') get totalDistanceKm() { + return this.total_distance_m ? (this.total_distance_m / 1000).toFixed(1) : '0.0'; + } + + @computed('total_duration_s') get totalDurationFormatted() { + if (!this.total_duration_s) { + return '0m'; + } + const hours = Math.floor(this.total_duration_s / 3600); + const minutes = Math.floor((this.total_duration_s % 3600) / 60); + return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + } + + @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('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 createdAgo() { + if (!isValidDate(this.created_at)) { + return null; + } + return formatDistanceToNow(this.created_at); + } + + @computed('scheduled_date') get scheduledDateFormatted() { + if (!isValidDate(this.scheduled_date)) { + return null; + } + return formatDate(this.scheduled_date, 'dd MMM yyyy'); + } +} diff --git a/addon/models/order.js b/addon/models/order.js index f4c0da7..890a49d 100644 --- a/addon/models/order.js +++ b/addon/models/order.js @@ -22,6 +22,7 @@ export default class OrderModel extends Model { @attr('string') purchase_rate_uuid; @attr('string') tracking_number_uuid; @attr('string') driver_assigned_uuid; + @attr('string') manifest_uuid; @attr('string') service_quote_uuid; @attr('string') order_config_uuid; @attr('string') payload_id; @@ -37,6 +38,7 @@ export default class OrderModel extends Model { @belongsTo('payload', { async: false }) payload; @belongsTo('driver', { async: false, inverse: 'jobs' }) driver_assigned; @belongsTo('vehicle', { async: false }) vehicle_assigned; + @belongsTo('manifest', { async: false }) manifest; @belongsTo('route', { async: false }) route; @belongsTo('purchase-rate', { async: false }) purchase_rate; @belongsTo('tracking-number', { async: false }) tracking_number; diff --git a/addon/models/part.js b/addon/models/part.js index 25b5401..decea97 100644 --- a/addon/models/part.js +++ b/addon/models/part.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class PartModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') vendor_uuid; @@ -26,16 +27,22 @@ export default class PartModel extends Model { @attr('string') barcode; @attr('string') description; @attr('number') quantity_on_hand; - @attr('number') unit_cost; - @attr('number') msrp; + @attr('string') unit_cost; + @attr('string') msrp; + @attr('string') currency; @attr('string') type; @attr('string') status; @attr('raw') specs; @attr('raw') meta; @attr('string') slug; + /** @server-computed (read-only appended attributes) */ @attr('string') vendor_name; @attr('string') warranty_name; @attr('string') photo_url; + @attr('string') total_value; + @attr('boolean') is_in_stock; + @attr('boolean') is_low_stock; + @attr('string') asset_name; /** @dates */ @attr('date') deleted_at; diff --git a/addon/models/sensor.js b/addon/models/sensor.js index 84f2a86..911d1ac 100644 --- a/addon/models/sensor.js +++ b/addon/models/sensor.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class SensorModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') telematic_uuid; @@ -21,7 +22,6 @@ export default class SensorModel extends Model { /** @attributes */ @attr('string') name; - @attr('string') photo_url; @attr('string') internal_id; @attr('string') type; @attr('string') serial_number; @@ -29,11 +29,9 @@ export default class SensorModel extends Model { @attr('string') imsi; @attr('string') firmware_version; @attr('string') unit; - @attr('string') threshold_status; @attr('number') min_threshold; @attr('number') max_threshold; @attr('boolean') threshold_inclusive; - @attr('boolean') is_active; @attr('string') last_value; @attr('number') report_frequency_sec; @attr('point') last_position; @@ -42,6 +40,11 @@ export default class SensorModel extends Model { @attr('string') slug; @attr('string', { defaultValue: 'inactive' }) status; + /** @server-computed (read-only appended attributes) */ + @attr('string') photo_url; + @attr('boolean') is_active; + @attr('string') threshold_status; + /** @dates */ @attr('date') last_reading_at; @attr('date') deleted_at; diff --git a/addon/models/warranty.js b/addon/models/warranty.js index e6a7bfb..85fb37a 100644 --- a/addon/models/warranty.js +++ b/addon/models/warranty.js @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro export default class WarrantyModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; @attr('string') subject_type; @@ -21,7 +22,15 @@ export default class WarrantyModel extends Model { @attr('raw') policy; @attr('raw') meta; @attr('string') slug; + + /** @server-computed (read-only appended attributes) */ @attr('string') vendor_name; + @attr('string') subject_name; + @attr('boolean') is_active; + @attr('boolean') is_expired; + @attr('number') days_remaining; + @attr('string') coverage_summary; + @attr('string') status; /** @dates */ @attr('date') start_date; diff --git a/addon/models/work-order.js b/addon/models/work-order.js index 4b8488d..c60c454 100644 --- a/addon/models/work-order.js +++ b/addon/models/work-order.js @@ -1,19 +1,20 @@ -import Model, { attr } from '@ember-data/model'; +import Model, { attr, belongsTo } from '@ember-data/model'; import { computed } from '@ember/object'; import { format as formatDate, isValid as isValidDate, formatDistanceToNow } from 'date-fns'; export default class WorkOrderModel extends Model { /** @ids */ + @attr('string') uuid; @attr('string') public_id; @attr('string') company_uuid; - @attr('string') target_type; - @attr('string') target_uuid; - @attr('string') assignee_type; - @attr('string') assignee_uuid; + @attr('string') schedule_uuid; - /** @relationships */ - // Note: relationships would be polymorphic (target, assignee) - // but not explicitly defined in Ember Data for morphTo + /** @polymorphic relationships */ + @belongsTo('maintenance-subject', { polymorphic: true, async: false }) target; + @belongsTo('facilitator', { polymorphic: true, async: false }) assignee; + /** @computed names — server-side convenience fields (read-only) */ + @attr('string') target_name; + @attr('string') assignee_name; /** @attributes */ @attr('string') code; @@ -22,8 +23,20 @@ export default class WorkOrderModel extends Model { @attr('string') priority; @attr('string') instructions; @attr('raw') checklist; + @attr('string') estimated_cost; + @attr('string') approved_budget; + @attr('string') actual_cost; + @attr('string') currency; + @attr('raw') cost_breakdown; + @attr('string') cost_center; + @attr('string') budget_code; @attr('raw') meta; - @attr('string') slug; + + /** @server-computed (read-only appended attributes) */ + @attr('boolean') is_overdue; + @attr('number') days_until_due; + @attr('number') completion_percentage; + @attr('number') estimated_duration; /** @dates */ @attr('date') opened_at; diff --git a/addon/serializers/maintenance-schedule.js b/addon/serializers/maintenance-schedule.js new file mode 100644 index 0000000..482e69c --- /dev/null +++ b/addon/serializers/maintenance-schedule.js @@ -0,0 +1,66 @@ +import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; +import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; +import { isBlank } from '@ember/utils'; + +export default class MaintenanceScheduleSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + /** + * Embedded relationship attributes. + * + * @var {Object} + */ + get attrs() { + return { + subject: { embedded: 'always' }, + default_assignee: { embedded: 'always' }, + }; + } + + /** + * Serialize the record and strip read-only server-computed attributes. + * + * @param {Snapshot} snapshot + * @param {Object} options + * @returns {Object} + */ + serialize(snapshot, options) { + const json = super.serialize(snapshot, options); + const readOnly = ['subject_name', 'default_assignee_name']; + readOnly.forEach((attr) => delete json[attr]); + return json; + } + + /** + * Serialize the polymorphic type for subject and default_assignee relationships. + * + * The embedded record's modelName will be the concrete subtype (e.g. 'facilitator-vendor', + * 'maintenance-subject-vehicle'). We strip the abstract prefix before sending to the server + * so that getMutationType resolves the correct PHP class (e.g. Vendor, Vehicle). + * + * @param {Snapshot} snapshot + * @param {Object} json + * @param {Object} relationship + */ + serializePolymorphicType(snapshot, json, relationship) { + let key = relationship.key; + let belongsTo = snapshot.belongsTo(key); + + const isPolymorphicTypeBlank = isBlank(snapshot.attr(key + '_type')); + if (isPolymorphicTypeBlank) { + key = this.keyForAttribute ? this.keyForAttribute(key, 'serialize') : key; + if (!belongsTo) { + json[key + '_type'] = null; + } else { + let type = belongsTo.modelName; + if (!isBlank(belongsTo.attr(`${key}_type`))) { + type = belongsTo.attr(`${key}_type`); + } + // Strip abstract subtype prefixes so the server receives the bare model type + // e.g. 'facilitator-vendor' -> 'vendor', 'maintenance-subject-vehicle' -> 'vehicle' + if (typeof type === 'string') { + type = type.replace(/^facilitator-/, '').replace(/^maintenance-subject-/, '').replace(/^customer-/, ''); + } + json[key + '_type'] = `fleet-ops:${type}`; + } + } + } +} diff --git a/addon/serializers/maintenance.js b/addon/serializers/maintenance.js index 278a63b..345e63b 100644 --- a/addon/serializers/maintenance.js +++ b/addon/serializers/maintenance.js @@ -1,4 +1,68 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; +import { isBlank } from '@ember/utils'; -export default class MaintenanceSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {} +export default class MaintenanceSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + /** + * Embedded relationship attributes. + * + * @var {Object} + */ + get attrs() { + return { + maintainable: { embedded: 'always' }, + performed_by: { embedded: 'always' }, + work_order: { embedded: 'always' }, + custom_field_values: { embedded: 'always' }, + }; + } + + /** + * Serialize the record and strip read-only server-computed attributes. + * + * @param {Snapshot} snapshot + * @param {Object} options + * @returns {Object} + */ + serialize(snapshot, options) { + const json = super.serialize(snapshot, options); + const readOnly = ['maintainable_name', 'performed_by_name', 'work_order_subject', 'duration_hours', 'is_overdue', 'days_until_due', 'cost_breakdown']; + readOnly.forEach((attr) => delete json[attr]); + return json; + } + + /** + * Serialize the polymorphic type for maintainable and performed_by relationships. + * + * The embedded record's modelName will be the concrete subtype (e.g. 'facilitator-vendor', + * 'maintenance-subject-vehicle'). We strip the abstract prefix before sending to the server + * so that getMutationType resolves the correct PHP class (e.g. Vendor, Vehicle). + * + * @param {Snapshot} snapshot + * @param {Object} json + * @param {Object} relationship + */ + serializePolymorphicType(snapshot, json, relationship) { + let key = relationship.key; + let belongsTo = snapshot.belongsTo(key); + + const isPolymorphicTypeBlank = isBlank(snapshot.attr(key + '_type')); + if (isPolymorphicTypeBlank) { + key = this.keyForAttribute ? this.keyForAttribute(key, 'serialize') : key; + if (!belongsTo) { + json[key + '_type'] = null; + } else { + let type = belongsTo.modelName; + if (!isBlank(belongsTo.attr(`${key}_type`))) { + type = belongsTo.attr(`${key}_type`); + } + // Strip abstract subtype prefixes so the server receives the bare model type + // e.g. 'facilitator-vendor' -> 'vendor', 'maintenance-subject-vehicle' -> 'vehicle' + if (typeof type === 'string') { + type = type.replace(/^facilitator-/, '').replace(/^maintenance-subject-/, '').replace(/^customer-/, ''); + } + json[key + '_type'] = `fleet-ops:${type}`; + } + } + } +} diff --git a/addon/serializers/manifest-stop.js b/addon/serializers/manifest-stop.js new file mode 100644 index 0000000..20cde8b --- /dev/null +++ b/addon/serializers/manifest-stop.js @@ -0,0 +1,9 @@ +import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; +import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; + +export default class ManifestStopSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + attrs = { + order: { embedded: 'always' }, + place: { embedded: 'always' }, + }; +} diff --git a/addon/serializers/manifest.js b/addon/serializers/manifest.js new file mode 100644 index 0000000..0e134ef --- /dev/null +++ b/addon/serializers/manifest.js @@ -0,0 +1,10 @@ +import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; +import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; + +export default class ManifestSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + attrs = { + driver: { embedded: 'always' }, + vehicle: { embedded: 'always' }, + stops: { embedded: 'always' }, + }; +} diff --git a/addon/serializers/order.js b/addon/serializers/order.js index 438bd04..3e00bac 100644 --- a/addon/serializers/order.js +++ b/addon/serializers/order.js @@ -75,20 +75,24 @@ export default class OrderSerializer extends ApplicationSerializer.extend(Embedd serializePolymorphicType(snapshot, json, relationship) { let key = relationship.key; let belongsTo = snapshot.belongsTo(key); - let type = belongsTo.modelName; // if snapshot already has type filled respect manual input const isPolymorphicTypeBlank = isBlank(snapshot.attr(key + '_type')); if (isPolymorphicTypeBlank) { key = this.keyForAttribute ? this.keyForAttribute(key, 'serialize') : key; - if (!isBlank(belongsTo.attr(`${key}_type`))) { - type = belongsTo.attr(`${key}_type`); - } - if (!belongsTo) { json[key + '_type'] = null; } else { + let type = belongsTo.modelName; + if (!isBlank(belongsTo.attr(`${key}_type`))) { + type = belongsTo.attr(`${key}_type`); + } + // Strip abstract subtype prefixes so the server receives the bare model type + // e.g. 'facilitator-vendor' -> 'vendor', 'customer-contact' -> 'contact' + if (typeof type === 'string') { + type = type.replace(/^facilitator-/, '').replace(/^customer-/, '').replace(/^maintenance-subject-/, ''); + } json[key + '_type'] = `fleet-ops:${type}`; } } diff --git a/addon/serializers/vendor.js b/addon/serializers/vendor.js index 24cb17c..1aec886 100644 --- a/addon/serializers/vendor.js +++ b/addon/serializers/vendor.js @@ -3,13 +3,20 @@ import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; export default class VendorSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { /** - * Embedded relationship attributes + * Embedded relationship attributes. + * + * `personnels` must be declared as embedded so that the EmbeddedRecordsMixin + * resolves each record using the declared `@hasMany('contact')` model type + * rather than attempting to look up the model by the raw `type` field on + * each payload object (which carries a backend STI discriminator such as + * `fliit_contact` that does not exist in the Ember Data model registry). * * @var {Object} */ get attrs() { return { place: { embedded: 'always' }, + personnels: { embedded: 'always' }, custom_field_values: { embedded: 'always' }, }; } diff --git a/addon/serializers/work-order.js b/addon/serializers/work-order.js index 6897d0f..908ea4a 100644 --- a/addon/serializers/work-order.js +++ b/addon/serializers/work-order.js @@ -1,4 +1,67 @@ import ApplicationSerializer from '@fleetbase/ember-core/serializers/application'; import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; +import { isBlank } from '@ember/utils'; -export default class WorkOrderSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {} +export default class WorkOrderSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) { + /** + * Embedded relationship attributes. + * + * @var {Object} + */ + get attrs() { + return { + target: { embedded: 'always' }, + assignee: { embedded: 'always' }, + custom_field_values: { embedded: 'always' }, + }; + } + + /** + * Serialize the record and strip read-only server-computed attributes. + * + * @param {Snapshot} snapshot + * @param {Object} options + * @returns {Object} + */ + serialize(snapshot, options) { + const json = super.serialize(snapshot, options); + const readOnly = ['target_name', 'assignee_name', 'is_overdue', 'days_until_due', 'completion_percentage']; + readOnly.forEach((attr) => delete json[attr]); + return json; + } + + /** + * Serialize the polymorphic type for target and assignee relationships. + * + * The embedded record's modelName will be the concrete subtype (e.g. 'facilitator-vendor', + * 'maintenance-subject-vehicle'). We strip the abstract prefix before sending to the server + * so that getMutationType resolves the correct PHP class (e.g. Vendor, Vehicle). + * + * @param {Snapshot} snapshot + * @param {Object} json + * @param {Object} relationship + */ + serializePolymorphicType(snapshot, json, relationship) { + let key = relationship.key; + let belongsTo = snapshot.belongsTo(key); + + const isPolymorphicTypeBlank = isBlank(snapshot.attr(key + '_type')); + if (isPolymorphicTypeBlank) { + key = this.keyForAttribute ? this.keyForAttribute(key, 'serialize') : key; + if (!belongsTo) { + json[key + '_type'] = null; + } else { + let type = belongsTo.modelName; + if (!isBlank(belongsTo.attr(`${key}_type`))) { + type = belongsTo.attr(`${key}_type`); + } + // Strip abstract subtype prefixes so the server receives the bare model type + // e.g. 'facilitator-vendor' -> 'vendor', 'maintenance-subject-vehicle' -> 'vehicle' + if (typeof type === 'string') { + type = type.replace(/^facilitator-/, '').replace(/^maintenance-subject-/, '').replace(/^customer-/, ''); + } + json[key + '_type'] = `fleet-ops:${type}`; + } + } + } +} diff --git a/app/adapters/manifest-stop.js b/app/adapters/manifest-stop.js new file mode 100644 index 0000000..28bc5ab --- /dev/null +++ b/app/adapters/manifest-stop.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/adapters/manifest-stop'; diff --git a/app/adapters/manifest.js b/app/adapters/manifest.js new file mode 100644 index 0000000..621fff9 --- /dev/null +++ b/app/adapters/manifest.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/adapters/manifest'; diff --git a/app/models/maintenance-schedule.js b/app/models/maintenance-schedule.js new file mode 100644 index 0000000..5bc42d0 --- /dev/null +++ b/app/models/maintenance-schedule.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/models/maintenance-schedule'; diff --git a/app/models/maintenance-subject-equipment.js b/app/models/maintenance-subject-equipment.js new file mode 100644 index 0000000..cbca1e1 --- /dev/null +++ b/app/models/maintenance-subject-equipment.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/models/maintenance-subject-equipment'; diff --git a/app/models/maintenance-subject-vehicle.js b/app/models/maintenance-subject-vehicle.js new file mode 100644 index 0000000..bc889fa --- /dev/null +++ b/app/models/maintenance-subject-vehicle.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/models/maintenance-subject-vehicle'; diff --git a/app/models/maintenance-subject.js b/app/models/maintenance-subject.js new file mode 100644 index 0000000..1c1eb3b --- /dev/null +++ b/app/models/maintenance-subject.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/models/maintenance-subject'; diff --git a/app/models/manifest-stop.js b/app/models/manifest-stop.js new file mode 100644 index 0000000..db4f556 --- /dev/null +++ b/app/models/manifest-stop.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/models/manifest-stop'; diff --git a/app/models/manifest.js b/app/models/manifest.js new file mode 100644 index 0000000..bd9b68a --- /dev/null +++ b/app/models/manifest.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/models/manifest'; 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'; diff --git a/app/serializers/manifest-stop.js b/app/serializers/manifest-stop.js new file mode 100644 index 0000000..b3ea7bc --- /dev/null +++ b/app/serializers/manifest-stop.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/serializers/manifest-stop'; diff --git a/app/serializers/manifest.js b/app/serializers/manifest.js new file mode 100644 index 0000000..e6e2212 --- /dev/null +++ b/app/serializers/manifest.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/serializers/manifest';