feat(maintenance): Complete maintenance module — Maintenances, Work Orders, Equipment & Parts#215
Open
feat(maintenance): Complete maintenance module — Maintenances, Work Orders, Equipment & Parts#215
Conversation
…k orders, equipment & parts Completes the existing but incomplete maintenance section in FleetOps, enabling full CRUD for all four maintenance resources: Maintenances, Work Orders, Equipment, and Parts. Frontend: - Uncomment and activate the maintenance sidebar panel (fleet-ops-sidebar.js) - Add Maintenances as first sidebar item with wrench icon - Add maintenances route block to routes.js - New route files for maintenances (index, new, edit, details, details/index) - Fix all work-orders/equipment/parts details+edit routes: add model hook and permission guard - New controllers for maintenances (index, new, edit, details, details/index) - Complete controllers for work-orders, equipment, parts (full columns, save tasks, tabs, action buttons) - New panel-header components for all four resources (HBS + JS) - Fix all new.hbs templates: correct @resource binding (was this.place bug) - Fix all details.hbs: add @headerComponent, TabNavigation with outlet - Fix all edit.hbs: add @headerTitle with resource name - New maintenances templates (maintenances.hbs, index, new, edit, details, details/index) - Add 12 new Universe registries in extension.js for maintenance/work-order/equipment/part - Fix maintenance-actions.js: use maintenance.summary instead of maintenance.name Backend: - Add maintenance, work-order, equipment, part resources to FleetOps auth schema - Add MaintenanceManager policy with full CRUD on all four resources - Update OperationsAdmin policy to include all four maintenance resources - Add Maintenance Technician role - New ProcessMaintenanceTriggers artisan command (time-based + odometer/engine-hour triggers) - Register command and daily schedule in FleetOpsServiceProvider
… sections - work-order/form: split into Identification, Assignment (polymorphic target + assignee with type-driven ModelSelect), Scheduling, and Instructions panels. Added targetTypeOptions, assigneeTypeOptions, onTargetTypeChange, onAssigneeTypeChange, assignTarget, assignAssignee actions. Removed hardcoded 'user' model assumption. - maintenance/form: split into Identification, Asset & Work Order (polymorphic maintainable + performed-by), Scheduling & Readings (odometer, engine_hours, scheduled_at, started_at, completed_at), Costs (MoneyInput for labor_cost, parts_cost, tax, total_cost), and Notes panels. Added full polymorphic type handlers. - equipment/form: split into Photo, Identification (name, code, serial_number, manufacturer, model, type, status), Assignment (polymorphic equipable), and Purchase & Warranty panels. Fixed photo upload to use fetch.uploadFile.perform pattern. Added onEquipableTypeChange / assignEquipable actions. - part/form: split into Photo, Identification (name, sku, serial_number, barcode, manufacturer, model, type, status, description), Inventory (quantity_on_hand, unit_cost, msrp with MoneyInput), Compatibility (polymorphic asset), and Vendor & Warranty panels. Fixed photo upload to use fetch.uploadFile.perform pattern. Added onAssetTypeChange / assignAsset actions. All forms: added MetadataEditor panel, RegistryYield hooks, and CustomField::Yield. All option arrays cross-checked against PHP model fillable arrays and fleetops-data Ember models.
… equipment route name
… and part forms, fix all ContentPanel wrapperClass - equipment/form.hbs: remove standalone Photo ContentPanel; photo block (Image + UploadButton, matching vehicle/form structure) is now the first child of the Identification ContentPanel before the field grid. - part/form.hbs: same restructure as equipment. - All four forms (work-order, maintenance, equipment, part): every ContentPanel now carries @wrapperclass="bordered-top", including the first panel. Previously work-order and maintenance first panels had no wrapperClass at all. - equipment/form.js: equipableTypeOptions converted to { value, label } objects; added @Tracked selectedEquipableType; onEquipableTypeChange now receives option object and reads option.value. - part/form.js: assetTypeOptions converted to { value, label } objects; added @Tracked selectedAssetType; onAssetTypeChange updated similarly. - Both HBS files updated to bind @selected to the tracked option object and render {{option.label}} in the PowerSelect block.
…d-by type selectors
- maintainableTypeOptions: plain strings -> { value, label } objects
(Vehicle, Equipment)
- performedByTypeOptions: plain strings -> { value, label } objects
(Vendor, Driver, User) — added Vendor as a valid performer type
- Added selectedMaintainableType and selectedPerformedByType tracked
properties so the PowerSelect trigger shows the human-readable label
- Both onChange actions now receive the full option object and write
option.value to the model attribute
- Updated TYPE_TO_MODEL to include fleet-ops:vendor -> vendor
- HBS PowerSelect @selected bindings updated to use the tracked option
objects; block params renamed from |type| to |option| with {{option.label}}
…ems panel - Add migration to add public_id column to maintenances, work_orders, equipment, and parts tables (fixes SQLSTATE[42S22] unknown column error) - Replace flat cost ContentPanel with new Maintenance::CostPanel component - Invoice-style line items table with description, qty, unit cost, line total - Inline add/edit/remove rows with optimistic UI updates - Labour and Tax inputs remain as direct MoneyInput fields - Computed totals summary (Labour + Parts + Tax = Total) - All mutations hit dedicated API endpoints and reflect server-recomputed totals - Add addLineItem / updateLineItem / removeLineItem endpoints to MaintenanceController - Register POST/PUT/DELETE line-item sub-routes in routes.php
Resolves Glimmer reactivity assertion error caused by MoneyInput's autoNumerize modifier consuming and mutating @resource.currency in the same render cycle. Following the vehicle/form.hbs pattern, each form now has a single CurrencySelect input at the top of the cost/pricing section. All MoneyInput fields simply read @Currency without @canSelectCurrency or @onCurrencyChange. Files changed: - maintenance/cost-panel.hbs: added CurrencySelect before labour/tax inputs; removed @canSelectCurrency from all MoneyInput fields - equipment/form.hbs: added CurrencySelect before purchase_price; removed @canSelectCurrency/@onCurrencyChange from purchase_price - part/form.hbs: added CurrencySelect before unit_cost/msrp; removed @canSelectCurrency/@onCurrencyChange from both fields
…se/fleetops into feat/complete-maintenance-module
- Add Equipment::Card component (photo, type, status, year, quick actions) - Add Part::Card component (photo, type, qty, unit cost, quick actions) - Equipment index controller: inject appCache, add @Tracked layout, convert actionButtons/bulkActions to getters, add layout toggle dropdown - Parts index controller: same pattern as Equipment - Equipment index template: conditional table vs CardsGrid layout - Parts index template: conditional table vs CardsGrid layout - Layout preference persisted via appCache (fleetops:equipment:layout, fleetops:parts:layout)
Vehicle row dropdown additions: - Schedule Maintenance → opens schedule form pre-filled with vehicle - Create Work Order → opens work order form pre-filled with vehicle - Log Maintenance → opens maintenance form pre-filled with vehicle Vehicle details panel — 3 new tabs: - Schedules: lists active maintenance schedules for the vehicle, empty state with 'Add Schedule' CTA - Work Orders: lists work orders targeting the vehicle, empty state with 'Create Work Order' CTA - Maintenance History: lists completed maintenance records, empty state with 'Log Maintenance' CTA Supporting changes: - vehicle-actions.js: inject scheduleActions/workOrderActions/maintenanceActions, add scheduleMaintenance/createWorkOrder/logMaintenance @action methods - routes.js: add schedules/work-orders/maintenance-history sub-routes under vehicles.index.details; add maintenance.schedules top-level route - Translations: add vehicle.actions.schedule-maintenance/create-work-order/ log-maintenance; add menu.schedules/maintenance-history; add resource.maintenance-schedule(s)
…mmand rewrite Backend changes: - Migration: create maintenance_schedules table with interval fields (time/distance/engine-hours), next-due thresholds, default assignee, and add schedule_uuid FK to work_orders for traceability - MaintenanceSchedule model: isDue(), resetAfterCompletion(), pause(), resume(), complete() methods; polymorphic subject + defaultAssignee relationships; workOrders() hasMany - WorkOrderObserver: on status → 'closed', auto-creates a Maintenance history record from completion data stored in work_order.meta and calls schedule.resetAfterCompletion() to restart the interval cycle - ProcessMaintenanceTriggers rewrite: now reads MaintenanceSchedule instead of Maintenance; resolves vehicle odometer/engine-hours from the polymorphic subject; skips schedules with an existing open WO; auto-creates WorkOrder from schedule defaults on trigger - MaintenanceScheduleController: CRUD via FleetOpsController base + custom pause/resume/trigger endpoints - routes.php: register maintenance-schedules routes with pause/resume/ trigger sub-routes before work-orders - FleetOpsServiceProvider: register WorkOrderObserver
…r update, WO completion panel Frontend changes: - Sidebar: add 'Schedules' (calendar-alt icon) as first item in the Maintenance panel; rename 'Maintenances' entry to 'Maintenance History' (history icon) — order is now: Schedules, Work Orders, Maintenance History, Equipment, Parts - MaintenanceSchedule Ember model: full attr mapping for interval fields, next-due thresholds, default assignee, status, subject polymorphic - schedule-actions service: ResourceActionService subclass with transition/panel/modal patterns + pause(), resume(), triggerNow() actions - schedule/form.hbs + form.js: full create/edit form with Schedule Details, Asset (polymorphic subject), Maintenance Interval (time/distance/hours), and Work Order Defaults (priority, default assignee, instructions) panels - schedule/details.hbs + details.js: read-only details view component - Routes: maintenance.schedules.index (+ new/edit/details sub-routes) - Controllers: schedules/index (columns, actionButtons, bulkActions), schedules/index/details (tabs, actionButtons, edit/triggerNow/delete), schedules/index/new, schedules/index/edit - Templates: schedules index (Layout::Resource::Tabular), new overlay, edit overlay, details overlay - work-order/form.hbs: add Completion Details panel (odometer, engine hours, labour cost, parts cost, tax, notes) shown only when status is set to 'closed'; seeds the WorkOrderObserver auto-log creation - work-order/form.js: add isCompleting getter + six @Tracked completion state fields
… under MySQL 64-char limit The auto-generated name 'maintenance_schedules_default_assignee_type_default_assignee_uuid_index' is 73 characters, exceeding MySQL's 64-character identifier limit. Replaced with explicit short name 'ms_default_assignee_idx'.
…edit/details, fix TYPE_TO_MODEL keys, complete new/edit controllers with save task
…Select displayName, translation keys
…nce_schedules table
… is a real DB column not a computed accessor
…/cell/base across all maintenance controllers
…rvice, calendar, namespace 1. Rename scheduleActions → maintenanceScheduleActions - addon/services/schedule-actions.js → maintenance-schedule-actions.js - app/services/maintenance-schedule-actions.js re-export added - All @service injections and this.scheduleActions refs updated in schedules/index, schedules/index/details, vehicle-actions 2. Convert @Tracked actionButtons/bulkActions/columns → getters - All 5 maintenance index controllers now use get() instead of @Tracked - Prevents Glimmer reactivity assertion errors on render 3. Fix broken @service menuService injection - All 5 details controllers: @service menuService → @service('universe/menu-service') menuService 4. Rename schedule/ component namespace → maintenance-schedule/ - addon/components/schedule/ → addon/components/maintenance-schedule/ - app/components/maintenance-schedule/ re-exports added - Templates updated: Schedule::Form/Details → MaintenanceSchedule::Form/Details - Class names updated to MaintenanceScheduleFormComponent etc. 5. Add calendar visualization to MaintenanceSchedule::Details - details.js: computeOccurrences() + buildCalendarGrid() helpers - Navigable month calendar with scheduled dates highlighted in blue - Upcoming occurrences list (next 6 dates) - Only shown for time-based schedules (interval_method === 'time')
…-orders index getters The sed-based getter conversion left actionButtons and bulkActions getters without their closing } in two controllers: - maintenances/index.js: actionButtons and bulkActions both missing } - work-orders/index.js: bulkActions missing } schedules/index.js, equipment/index.js, and parts/index.js were unaffected.
…O tab, vehicle prefill, cost-panel re-export - ProcessMaintenanceTriggers: auto-generate WO code (WO-YYYYMMDD-XXXXX) and set opened_at on creation - WorkOrder::Details: full details component with overview, assignment, scheduling, and cost breakdown panels (cost breakdown reads from meta.completion_data, shown only when status is closed) - WorkOrder::Form: add prepareForSave action that packs completion tracked fields into meta before save - work-orders new/edit controllers: track formComponent and call prepareForSave before workOrder.save() - Schedules details: add Work Orders tab (route + template) showing all WOs created by this schedule - vehicle-actions: fix subject_type to use namespaced type strings (fleet-ops:vehicle etc) so schedule form pre-selects the correct asset type when opened from the vehicles index row dropdown - app/components/maintenance/cost-panel.js: add missing re-export shim - app/components/maintenance/panel-header.js: add missing re-export shim
…hip accessors
Replace all raw _type / _uuid attr reads and writes with proper
@belongsTo relationship accessors across the maintenance module.
Changes:
- addon/models/maintenance-schedule.js
• Replace subject_type/subject_uuid/subject_name attrs with
@belongsTo('maintenance-subject', {polymorphic:true}) subject
• Replace default_assignee_type/default_assignee_uuid attrs with
@belongsTo('facilitator', {polymorphic:true}) default_assignee
• Add interval_method attr (was missing)
• Remove obsolete raw type/uuid attrs
- addon/components/maintenance-schedule/form.js
• Add MODEL_TO_TYPE + ASSIGNEE_MODEL_TO_TYPE reverse-lookup maps
• Constructor now reads type from resource.subject.constructor.modelName
and resource.default_assignee.constructor.modelName instead of raw attrs
• onSubjectTypeChange / onAssigneeTypeChange clear the relationship
instead of writing _type/_uuid
• assignSubject / assignDefaultAssignee set the relationship only
- addon/components/maintenance-schedule/form.hbs
• @selectedModel binding updated from defaultAssignee → default_assignee
- addon/components/maintenance-schedule/details.hbs
• Asset field reads subject.displayName|name instead of subject_name
- addon/components/work-order/form.js
• Add TARGET_MODEL_TO_TYPE + ASSIGNEE_MODEL_TO_TYPE reverse-lookup maps
• Constructor reads type from target/assignee relationship model names
• onTargetTypeChange / onAssigneeTypeChange clear relationship only
• assignTarget / assignAssignee set relationship only
- addon/components/work-order/details.hbs
• Assignment panel uses target.displayName / assignee.displayName
• Schedule panel uses schedule.name instead of schedule_uuid
- addon/components/maintenance/form.js
• Add MAINTAINABLE_MODEL_TO_TYPE + PERFORMED_BY_MODEL_TO_TYPE maps
• Constructor reads type from maintainable/performed_by relationship
• onMaintainableTypeChange / onPerformedByTypeChange clear relationship
• assignMaintainable / assignPerformedBy set relationship only
- addon/components/maintenance/form.hbs
• @selectedModel binding updated from performedBy → performed_by
- addon/components/maintenance/details.hbs
• Maintainable / Performed By fields use relationship accessors
- addon/services/vehicle-actions.js
• scheduleMaintenance: pass { subject: vehicle } only
• createWorkOrder: pass { target: vehicle } only
• logMaintenance: pass { maintainable: vehicle } only
- addon/components/vehicle/details/schedules.js
• Fix service injection: @service scheduleActions → @service('maintenance-schedule-actions')
- addon/components/vehicle/details/schedules.hbs
• Add Schedule button passes { subject: @vehicle }
- addon/components/vehicle/details/work-orders.hbs
• Create Work Order button passes { target: @vehicle }
- addon/components/vehicle/details/maintenance-history.hbs
• Log Maintenance button passes { maintainable: @vehicle }
…se/fleetops into feat/complete-maintenance-module
… non-embedded relationship display Three fixes applied: 1. Remove console.fleet-ops. route prefix (controllers + routes) All transitionTo and tab route strings in addon/controllers/maintenance/ and addon/routes/maintenance/ were incorrectly prefixed with 'console.fleet-ops.'. The host router adds the engine prefix automatically, so the routes should start with 'maintenance.' directly. Fixed across all 15 controller files and 8 route files. 2. Add _name fallback attrs to details templates The subject, default_assignee, target, assignee, maintainable, and performed_by relationships are not sideloaded in the server response. Details templates now use (or relationship.displayName relationship.name resource._name) so the server-side convenience field is shown when the relationship object has not yet been loaded. 3. Serializer embedded attrs cleanup (fleetops-data companion commit) Removed embedded: always declarations for non-sideloaded polymorphic relationships in maintenance-schedule, work-order, and maintenance serializers to prevent Ember Data from expecting nested objects that the server never returns.
…ic relationships Backend changes: - Add $with = ['subject', 'defaultAssignee'] to MaintenanceSchedule model - Add $with = ['target', 'assignee'] to WorkOrder model (remove from $hidden) - Add $with = ['maintainable', 'performedBy'] to Maintenance model (remove from $hidden) - Create Http/Resources/v1/MaintenanceSchedule.php resource transformer - Embeds subject and default_assignee via whenLoaded() - Outputs raw PHP class name for _type fields (serializer maps them) - Create Http/Resources/v1/WorkOrder.php resource transformer - Embeds target and assignee via whenLoaded() - Create Http/Resources/v1/Maintenance.php resource transformer - Embeds maintainable and performed_by via whenLoaded() The resource transformers ensure that polymorphic relationship objects are always included in API responses, enabling the frontend to use embedded: always in its serializers without needing a separate request to load the related records.
…formers All existing FleetbaseResource subclasses declare toArray($request) without a return type annotation. Adding ': array' caused PHP to dispatch withCustomFields() through __call() on the JsonResource base class instead of the concrete class, resulting in BadMethodCallException.
withCustomFields() is a method provided by the HasCustomFields trait on the model. FleetbaseResource proxies it via DelegatesToResource to the underlying model instance. WorkOrder and Maintenance already had the trait applied; MaintenanceSchedule was missing both the import and the use statement, causing the BadMethodCallException when the resource transformer called withCustomFields().
…c objects Ember Data resolves the model for an embedded polymorphic belongsTo by reading the 'type' field inside the embedded object. Without it, Ember Data fell back to the model's own 'type' attribute (e.g. 'fliit_asset') which is not a valid model name. Each resource transformer now: - Calls Utils::toEmberResourceType() to convert PHP class names to shorthands for the *_type fields on the parent record. - Injects type: 'maintenance-subject' (or 'facilitator') into the embedded object so Ember Data resolves the correct abstract polymorphic model. - Injects subject_type / facilitator_type into the embedded object with the concrete subtype (e.g. 'maintenance-subject-vehicle'). Mirrors the exact pattern used by Order::setFacilitatorType().
…ttern, add cents conversion
…se/fleetops into feat/complete-maintenance-module
… maintenance module - cost-panel.hbs: add @onchange to labor_cost, tax MoneyInput fields; add @onchange={{this.setDraftUnitCost}} to inline edit/add row MoneyInput; fix column widths using colgroup percentage layout; use format-currency helper (cents-based) throughout totals summary and read rows - cost-panel.js: add setDraftUnitCost @action to receive cents from MoneyInput @onchange; document that draftUnitCost and all monetary values are in cents; clarify startEdit loads unit_cost already in cents from API - part/form.hbs: add @onchange={{fn (mut @resource.unit_cost)}} and @onchange={{fn (mut @resource.msrp)}} to MoneyInput fields - equipment/form.hbs: add @onchange={{fn (mut @resource.purchase_price)}} to MoneyInput field - work-order-actions.js: fix prepareForSave to not double-convert cents; MoneyInput @onchange already emits cents so toCents (x100) was wrong; replaced with toIntCents (parseInt only) - server models: add Money cast for all monetary attributes (cents storage) - migration: fix parts table monetary columns to BIGINT for cents storage
… Order email
## Import Functionality (Equipment, Parts, Maintenances, Work Orders, Maintenance Schedules)
### Backend
- Added Import classes: EquipmentImport, PartImport, MaintenanceImport, WorkOrderImport, MaintenanceScheduleImport
- Each implements Laravel Excel's ToModel and WithHeadingRow contracts
- Resolves related records (vehicles, vendors, equipment) by name/public_id
- Monetary values expected in cents (integers) matching Money cast storage
- Added createFromImport() static method to all five Eloquent models
- Added import() action to all five HTTP controllers (EquipmentController, PartController, MaintenanceController, WorkOrderController, MaintenanceScheduleController)
- Registered POST import routes for all five resources in routes.php
- POST /work-orders/{id}/send route registered for Send Work Order feature
### Frontend
- Added Import toolbar button (type: magic, icon: upload) to all five index controllers
- Import templates must be uploaded to S3 at:
flb-assets.s3.ap-southeast-1.amazonaws.com/import-templates/
- Fleetbase_Equipment_Import_Template.xlsx
- Fleetbase_Part_Import_Template.xlsx
- Fleetbase_Maintenance_Import_Template.xlsx
- Fleetbase_Work_Order_Import_Template.xlsx
- Fleetbase_Maintenance_Schedule_Import_Template.xlsx
## Send Work Order Email
### Backend
- Added WorkOrderDispatched Mail class (server/src/Mail/WorkOrderDispatched.php)
- Added work-order-dispatched Blade email template
- Added sendEmail() action to WorkOrderController
- Resolves assignee email, validates vendor has email, sends mail, logs activity
### Frontend
- Added sendEmail @action to WorkOrderActionsService
- Shows confirmation modal before sending
- POSTs to work-orders/{id}/send
- Shows success/error notification
- Added 'Send Work Order to Vendor' row action in work-orders index controller
## Monetary Attribute Type Fix (fleetops-data)
- Note: @attr('string') fix for monetary fields is in fleetops-data repo (separate commit)
…se/fleetops into feat/complete-maintenance-module
1. Monetary getters (laborCost, tax, partsCost, totalCost, lineTotal, draftLineTotal) now wrap values with parseInt(numbersOnly(...)) via a _toCents() helper. Required because monetary attrs are @attr('string') on the Ember model. Without this, string arithmetic produced NaN. 2. Line item mutations (add, edit, remove) are now purely in-memory. Previous implementation made individual API requests to fleet-ops/maintenances/{id}/line-items which was broken for new unsaved records and used the wrong URI prefix. Now each mutation writes a new array back onto @resource.line_items via _commitItems() and recomputes parts_cost and total_cost locally. The parent form save() call persists everything in a single request for both create and edit flows. 3. Removed ember-concurrency tasks from addLineItem, saveEdit, removeLineItem - now plain @actions. Updated HBS to use plain fn/onClick instead of perform for the affected buttons. 4. Removed @service fetch injection (no longer needed).
… details
- Created modals/send-work-order.hbs — a component-based confirmation modal
that displays:
- Work order subject (or target_name fallback), public_id, status, and
due date so the user can confirm they have the correct work order
- Vendor card showing name, email, and phone so the user knows exactly
who the email will be sent to
- Amber warning when no vendor is assigned
- Red error banner when the vendor has no email address on file, with
the Send button disabled to prevent a failed send
- Created modals/send-work-order.js — minimal Glimmer component class
- Updated work-order-actions.js sendEmail action:
- Replaced modalsManager.confirm() with modalsManager.show() using the
new component-based modal
- Resolves vendorName, vendorEmail, vendorPhone from workOrder.assignee
with fallback to assignee_name for display-only contexts
- Disables the accept button when vendorEmail is absent
- Runs the POST request inside modal.startLoading()/modal.done() for
proper loading state feedback
- Updated work-orders/index/details.js:
- Added sendEmail @action that delegates to workOrderActions.sendEmail
- Added 'Send to Vendor' button (paper-plane icon) as the first
actionButton in the details panel toolbar
… on boot - Removed 'slug' from $fillable — the work_orders table has no slug column, causing the SQLSTATE[42S22] column not found error on insert - Added boot() method with a static::creating() hook that generates a unique WO-XXXXXXXX code (8 random uppercase chars) when no code is provided - Added Illuminate\Support\Str import for Str::random()
…and maintenances - Add public_id column directly into create_equipments_table, create_parts_table, create_work_orders_table, and create_maintenances_table migrations so the column is always present on fresh installs (HasPublicId::generatePublicId queries this column before insert; missing column caused SQLSTATE 42S22 errors) - The existing add_public_id_to_maintenance_tables migration already has Schema::hasColumn guards so it remains safe to run on both fresh and existing DBs - Remove photo_uuid from Equipment and Part fillable arrays (no such column exists in either table; caused SQLSTATE 42S22 on insert) - Remove slug from Maintenance fillable (no slug column in maintenances table) - Change unit_cost/msrp in create_parts_table migration from decimal(12,2) to bigInteger (cents) to match the Money cast and monetary storage standard - Guard fix_monetary_columns_in_parts_table migration with column type check so it is idempotent on fresh installs that already have bigInteger columns
- Revert the incorrect public_id additions to the original create_* migrations. Those migrations have already shipped in previous releases and would never re-run on existing installs. The existing add_public_id_to_maintenance_tables migration (with Schema::hasColumn guards) is the correct mechanism for existing deployments. - Add new migration 2026_04_01_000003_add_photo_uuid_to_equipment_and_parts_tables which adds the photo_uuid FK column to equipments and parts tables. Both models have a photo() BelongsTo relationship and getPhotoUrlAttribute() accessor but the backing column was missing from the original create migrations. The migration is guarded with Schema::hasTable and Schema::hasColumn checks so it is safe to run on both fresh and existing databases. - Restore photo_uuid to Equipment and Part $fillable arrays now that the column will exist after the migration runs.
… sectioning and invoice line items
…ble/performedBy in API response - Add setLineItemsAttribute mutator on Maintenance model using Money::apply() to strip currency symbols and convert formatted strings (e.g. 'S$100.00') to cents integers (e.g. 10000) before persisting to the line_items JSON column - Add onAfterCreate and onAfterUpdate hooks to MaintenanceController that call $record->load(['maintainable', 'performedBy']) so both polymorphic relationships are always embedded in the create/update API response without requiring the frontend to pass ?with= query params
Laravel's getMorphs() always derives the foreign-key column as {name}_id.
Every Fleetbase model uses {name}_uuid instead, so bare morphTo() calls
were silently looking up a non-existent column and returning null.
Fixed by passing __FUNCTION__, type_column, uuid_column to every morphTo():
Maintenance : maintainable() -> maintainable_type / maintainable_uuid
performedBy() -> performed_by_type / performed_by_uuid
WorkOrder : target() -> target_type / target_uuid
assignee() -> assignee_type / assignee_uuid
Equipment : equipable() -> equipable_type / equipable_uuid
Part : asset() -> asset_type / asset_uuid
Asset : assignedTo() -> assigned_to_type / assigned_to_uuid
operator() -> operator_type / operator_uuid
Device : attachable() -> attachable_type / attachable_uuid
Sensor : sensorable() -> sensorable_type / sensorable_uuid
Warranty : subject() -> subject_type / subject_uuid
This is the root cause of maintainable and performedBy always being null
in Maintenance API responses despite $with = ['maintainable','performedBy']
being declared on the model.
The original `add_public_id_to_maintenance_tables` migration (054932) referenced the table as 'equipment' (singular) instead of 'equipments' (plural — the actual table name declared in the Equipment model). Because that migration had already been recorded in the `migrations` table on existing deployments, Laravel would never re-run it, meaning the `equipments` table never received the `public_id` column. This caused the following error on every Equipment create/save: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'public_id' in 'where clause' (HasPublicId::generatePublicId uniqueness check) Fix: add a new, fully-idempotent migration `2026_04_01_000004_add_public_id_to_equipments_table` that uses Schema::hasTable + Schema::hasColumn guards before adding the column, so it is safe to run on both fresh installs (where the column may already exist from a corrected create migration) and existing databases that are missing the column.
The original create_parts_table migration omitted three columns that the Part model declares in $fillable and uses during inserts: - public_id : unique human-readable identifier (HasPublicId trait) - status : part lifecycle / stock status (e.g. 'in_stock') - currency : ISO 4217 code for monetary columns (unit_cost, msrp) Without these columns every Part creation attempt threw: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'status' in 'field list' The new idempotent migration adds all three columns with Schema::hasColumn guards so it is safe on both fresh installs and existing deployments.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This PR completes the existing but stub-only maintenance section in FleetOps. All four maintenance resources — Maintenances, Work Orders, Equipment, and Parts — now have fully functional CRUD flows, following the same conventions used throughout the rest of the extension (e.g. the
connectivitysection withLayout::Resource::Panel,TabNavigation, andpanel-headercomponents).The sidebar maintenance panel is also uncommented and activated.
Frontend Changes
Sidebar (
fleet-ops-sidebar.js)maintenance.maintenancesRoutes (
routes.js+ new route files)maintenancesroute block (index / new / edit / details / details.index)work-orders,equipment, andpartsdetails + edit routes: addedmodel()hook and permission guard (were empty stubs)Controllers
maintenances/index: full columns (summary, type, status, priority, scheduled_at, total_cost, created_at), action buttons, bulk delete, query paramsmaintenances/index/new: save task with event tracking and form resetmaintenances/index/edit: save task, unsaved-changes guard, cancel/view actionsmaintenances/index/details: tabs (Overview + registered extension tabs viamenuService.getMenuItems), edit/delete action buttonswork-orders/index,equipment/index,parts/index: full column definitions, action buttons, bulk actionsComponents
work-order/panel-header— displays code/subject, status badge, priority, assigned drivermaintenance/panel-header— displays summary, status badge, type, scheduled_atequipment/panel-header— displays photo, name, status badge, type, serial numberpart/panel-header— displays photo, name, status badge, type, part numberTemplates
new.hbsfiles: corrected@resource={{this.place}}bug → correct resource bindingdetails.hbsfiles: added@headerComponent(panel-header),TabNavigationwith{{outlet}}edit.hbsfiles: added@headerTitlewith resource namedetails/index.hbsfiles: wired to correct::Detailscomponentmaintenances(parent, index, new, edit, details, details/index)Extension Registries (
extension.js)Added 12 new Universe registries enabling downstream extensions to inject custom tabs and form sections:
Services
maintenance-actions.js: fixedmaintenance.name→maintenance.summaryin panel/modal titlesBackend Changes
Auth Schema (
server/src/Auth/Schemas/FleetOps.php)maintenance,work-order,equipment,part(each with standard CRUD + export/import actions)MaintenanceManagerpolicy: full CRUD on all four resourcesOperationsAdminpolicy: includes all four maintenance resourcesMaintenance Technicianrole: uses theMaintenanceManagerpolicyConsole Command (
server/src/Console/Commands/ProcessMaintenanceTriggers.php)New artisan command:
fleetops:process-maintenance-triggersscheduled_athas arrived and transitions them toin_progressodometerandengine_hoursagainstnext_service_odometer/next_service_engine_hourson scheduled maintenance recordsmaintenance.triggeredevent for downstream extension hooks (no hard-coded foreign keys to external modules)--sandboxand--dry-runflagsService Provider (
server/src/Providers/FleetOpsServiceProvider.php)ProcessMaintenanceTriggersin$commandsarrayfleetops:process-maintenance-triggersto run dailyExtensibility Design
All integration points are designed to be consumed by downstream extensions without modifying FleetOps core:
metaJSON column on all maintenance models accepts arbitrary key-value datamaintenance.triggeredevent can be listened to by any extension'sEventServiceProviderTesting Checklist
php artisan fleetops:process-maintenance-triggers --dry-runreports triggers without modifying recordsphp artisan fleetops:process-maintenance-triggerstransitions due maintenances toin_progress