From 4c768e2b3e063f5f569a3c4174c3b18067c9eaab Mon Sep 17 00:00:00 2001 From: Fleetbase Dev Date: Mon, 30 Mar 2026 04:11:08 -0400 Subject: [PATCH 01/51] feat(maintenance): complete maintenance module with maintenances, work 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 --- addon/components/equipment/panel-header.hbs | 29 +++ addon/components/equipment/panel-header.js | 2 + addon/components/layout/fleet-ops-sidebar.js | 16 +- addon/components/maintenance/panel-header.hbs | 31 ++++ addon/components/maintenance/panel-header.js | 2 + addon/components/part/panel-header.hbs | 29 +++ addon/components/part/panel-header.js | 2 + addon/components/work-order/panel-header.hbs | 31 ++++ addon/components/work-order/panel-header.js | 2 + .../maintenance/equipment/index.js | 117 +++---------- .../maintenance/equipment/index/details.js | 40 ++++- .../equipment/index/details/index.js | 1 - .../maintenance/equipment/index/edit.js | 57 +++++- .../maintenance/equipment/index/new.js | 33 +++- .../maintenance/maintenances/index.js | 165 ++++++++++++++++++ .../maintenance/maintenances/index/details.js | 53 ++++++ .../maintenances/index/details/index.js | 3 + .../maintenance/maintenances/index/edit.js | 68 ++++++++ .../maintenance/maintenances/index/new.js | 34 ++++ addon/controllers/maintenance/parts/index.js | 117 +++---------- .../maintenance/parts/index/details.js | 40 ++++- .../maintenance/parts/index/details/index.js | 1 - .../maintenance/parts/index/edit.js | 57 +++++- .../maintenance/parts/index/new.js | 33 +++- .../maintenance/work-orders/index.js | 114 +++--------- .../maintenance/work-orders/index/details.js | 41 ++++- .../maintenance/work-orders/index/edit.js | 57 +++++- .../maintenance/work-orders/index/new.js | 33 +++- addon/extension.js | 12 ++ addon/routes.js | 10 ++ .../maintenance/equipment/index/details.js | 28 ++- .../maintenance/equipment/index/edit.js | 28 ++- addon/routes/maintenance/maintenances.js | 3 + .../routes/maintenance/maintenances/index.js | 23 +++ .../maintenance/maintenances/index/details.js | 29 +++ .../maintenances/index/details/index.js | 3 + .../maintenance/maintenances/index/edit.js | 29 +++ .../maintenance/maintenances/index/new.js | 3 + .../routes/maintenance/parts/index/details.js | 28 ++- addon/routes/maintenance/parts/index/edit.js | 28 ++- .../maintenance/work-orders/index/details.js | 28 ++- .../maintenance/work-orders/index/edit.js | 28 ++- addon/services/maintenance-actions.js | 17 +- .../maintenance/equipment/index/details.hbs | 17 +- .../equipment/index/details/index.hbs | 3 +- .../maintenance/equipment/index/edit.hbs | 14 +- .../maintenance/equipment/index/new.hbs | 5 +- addon/templates/maintenance/maintenances.hbs | 1 + .../maintenance/maintenances/index.hbs | 14 ++ .../maintenances/index/details.hbs | 15 ++ .../maintenances/index/details/index.hbs | 1 + .../maintenance/maintenances/index/edit.hbs | 12 ++ .../maintenance/maintenances/index/new.hbs | 11 ++ .../maintenance/parts/index/details.hbs | 17 +- .../maintenance/parts/index/details/index.hbs | 3 +- .../maintenance/parts/index/edit.hbs | 14 +- .../templates/maintenance/parts/index/new.hbs | 5 +- .../maintenance/work-orders/index/details.hbs | 5 +- .../work-orders/index/details/index.hbs | 2 +- .../maintenance/work-orders/index/edit.hbs | 6 +- .../maintenance/work-orders/index/new.hbs | 5 +- server/src/Auth/Schemas/FleetOps.php | 44 +++++ .../Commands/ProcessMaintenanceTriggers.php | 125 +++++++++++++ .../src/Providers/FleetOpsServiceProvider.php | 2 + 64 files changed, 1477 insertions(+), 349 deletions(-) create mode 100644 addon/components/equipment/panel-header.hbs create mode 100644 addon/components/equipment/panel-header.js create mode 100644 addon/components/maintenance/panel-header.hbs create mode 100644 addon/components/maintenance/panel-header.js create mode 100644 addon/components/part/panel-header.hbs create mode 100644 addon/components/part/panel-header.js create mode 100644 addon/components/work-order/panel-header.hbs create mode 100644 addon/components/work-order/panel-header.js create mode 100644 addon/controllers/maintenance/maintenances/index.js create mode 100644 addon/controllers/maintenance/maintenances/index/details.js create mode 100644 addon/controllers/maintenance/maintenances/index/details/index.js create mode 100644 addon/controllers/maintenance/maintenances/index/edit.js create mode 100644 addon/controllers/maintenance/maintenances/index/new.js create mode 100644 addon/routes/maintenance/maintenances.js create mode 100644 addon/routes/maintenance/maintenances/index.js create mode 100644 addon/routes/maintenance/maintenances/index/details.js create mode 100644 addon/routes/maintenance/maintenances/index/details/index.js create mode 100644 addon/routes/maintenance/maintenances/index/edit.js create mode 100644 addon/routes/maintenance/maintenances/index/new.js create mode 100644 addon/templates/maintenance/maintenances.hbs create mode 100644 addon/templates/maintenance/maintenances/index.hbs create mode 100644 addon/templates/maintenance/maintenances/index/details.hbs create mode 100644 addon/templates/maintenance/maintenances/index/details/index.hbs create mode 100644 addon/templates/maintenance/maintenances/index/edit.hbs create mode 100644 addon/templates/maintenance/maintenances/index/new.hbs create mode 100644 server/src/Console/Commands/ProcessMaintenanceTriggers.php diff --git a/addon/components/equipment/panel-header.hbs b/addon/components/equipment/panel-header.hbs new file mode 100644 index 000000000..80a8453be --- /dev/null +++ b/addon/components/equipment/panel-header.hbs @@ -0,0 +1,29 @@ +
+
+
+
+ {{@resource.name}} +
+
+
+

{{@resource.name}}

+ {{smart-humanize @resource.status}} +
+
{{smart-humanize @resource.type}}
+
{{n-a @resource.serial_number}}
+
+
+
+ +
+
+
diff --git a/addon/components/equipment/panel-header.js b/addon/components/equipment/panel-header.js new file mode 100644 index 000000000..79e3438e1 --- /dev/null +++ b/addon/components/equipment/panel-header.js @@ -0,0 +1,2 @@ +import Component from '@glimmer/component'; +export default class EquipmentPanelHeaderComponent extends Component {} diff --git a/addon/components/layout/fleet-ops-sidebar.js b/addon/components/layout/fleet-ops-sidebar.js index f075267d0..8258843f1 100644 --- a/addon/components/layout/fleet-ops-sidebar.js +++ b/addon/components/layout/fleet-ops-sidebar.js @@ -183,6 +183,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component { ]; const maintenanceItems = [ + { + intl: 'menu.maintenances', + title: this.intl.t('menu.maintenances'), + icon: 'wrench', + route: 'maintenance.maintenances', + permission: 'fleet-ops list maintenance', + visible: this.abilities.can('fleet-ops see maintenance'), + }, { intl: 'menu.work-orders', title: this.intl.t('menu.work-orders'), @@ -289,10 +297,10 @@ export default class LayoutFleetOpsSidebarComponent extends Component { open: this.appCache.get('fleet-ops:sidebar:management:open', true), onToggle: (open) => this.appCache.set('fleet-ops:sidebar:management:open', open), }), - // createPanel('menu.maintenance', 'maintenance', maintenanceItems, { - // open: this.appCache.get('fleet-ops:sidebar:maintenance:open', false), - // onToggle: (open) => this.appCache.set('fleet-ops:sidebar:maintenance:open', open), - // }), + createPanel('menu.maintenance', 'maintenance', maintenanceItems, { + open: this.appCache.get('fleet-ops:sidebar:maintenance:open', false), + onToggle: (open) => this.appCache.set('fleet-ops:sidebar:maintenance:open', open), + }), createPanel('menu.connectivity', 'connectivity', connectivityItems, { open: this.appCache.get('fleet-ops:sidebar:connectivity:open', false), onToggle: (open) => this.appCache.set('fleet-ops:sidebar:connectivity:open', open), diff --git a/addon/components/maintenance/panel-header.hbs b/addon/components/maintenance/panel-header.hbs new file mode 100644 index 000000000..64b344487 --- /dev/null +++ b/addon/components/maintenance/panel-header.hbs @@ -0,0 +1,31 @@ +
+
+
+
+

{{n-a @resource.summary}}

+ {{smart-humanize @resource.status}} +
+
+
{{smart-humanize @resource.type}}
+ {{#if @resource.priority}} +
{{t "column.priority"}}: {{smart-humanize @resource.priority}}
+ {{/if}} + {{#if @resource.scheduled_at}} +
{{t "column.scheduled-at"}}: {{format-date @resource.scheduled_at}}
+ {{/if}} +
+
+
+ +
+
+
diff --git a/addon/components/maintenance/panel-header.js b/addon/components/maintenance/panel-header.js new file mode 100644 index 000000000..f15c64652 --- /dev/null +++ b/addon/components/maintenance/panel-header.js @@ -0,0 +1,2 @@ +import Component from '@glimmer/component'; +export default class MaintenancePanelHeaderComponent extends Component {} diff --git a/addon/components/part/panel-header.hbs b/addon/components/part/panel-header.hbs new file mode 100644 index 000000000..5c6a7d574 --- /dev/null +++ b/addon/components/part/panel-header.hbs @@ -0,0 +1,29 @@ +
+
+
+
+ {{@resource.name}} +
+
+
+

{{@resource.name}}

+ {{smart-humanize @resource.status}} +
+
{{smart-humanize @resource.type}}
+
{{n-a @resource.part_number}}
+
+
+
+ +
+
+
diff --git a/addon/components/part/panel-header.js b/addon/components/part/panel-header.js new file mode 100644 index 000000000..a148606fb --- /dev/null +++ b/addon/components/part/panel-header.js @@ -0,0 +1,2 @@ +import Component from '@glimmer/component'; +export default class PartPanelHeaderComponent extends Component {} diff --git a/addon/components/work-order/panel-header.hbs b/addon/components/work-order/panel-header.hbs new file mode 100644 index 000000000..c01683756 --- /dev/null +++ b/addon/components/work-order/panel-header.hbs @@ -0,0 +1,31 @@ +
+
+
+
+

{{or @resource.code @resource.subject}}

+ {{smart-humanize @resource.status}} +
+
+
{{smart-humanize @resource.type}}
+ {{#if @resource.priority}} +
{{t "column.priority"}}: {{smart-humanize @resource.priority}}
+ {{/if}} + {{#if @resource.assignee_name}} +
{{t "column.assignee"}}: {{@resource.assignee_name}}
+ {{/if}} +
+
+
+ +
+
+
diff --git a/addon/components/work-order/panel-header.js b/addon/components/work-order/panel-header.js new file mode 100644 index 000000000..7215e1af1 --- /dev/null +++ b/addon/components/work-order/panel-header.js @@ -0,0 +1,2 @@ +import Component from '@glimmer/component'; +export default class WorkOrderPanelHeaderComponent extends Component {} diff --git a/addon/controllers/maintenance/equipment/index.js b/addon/controllers/maintenance/equipment/index.js index 579bf7c6c..2535b6583 100644 --- a/addon/controllers/maintenance/equipment/index.js +++ b/addon/controllers/maintenance/equipment/index.js @@ -6,90 +6,31 @@ export default class MaintenanceEquipmentIndexController extends Controller { @service equipmentActions; @service intl; - /** query params */ - @tracked queryParams = ['name', 'page', 'limit', 'sort', 'query', 'public_id', 'created_at', 'updated_at']; + @tracked queryParams = ['type', 'status', 'page', 'limit', 'sort', 'query', 'public_id', 'created_at', 'updated_at']; @tracked page = 1; @tracked limit; @tracked sort = '-created_at'; @tracked public_id; - @tracked name; + @tracked type; + @tracked status; - /** action buttons */ @tracked actionButtons = [ - { - icon: 'refresh', - onClick: this.equipmentActions.refresh, - helpText: this.intl.t('common.refresh'), - }, - { - text: this.intl.t('common.new'), - type: 'primary', - icon: 'plus', - onClick: this.equipmentActions.transition.create, - }, - { - text: this.intl.t('common.import'), - type: 'magic', - icon: 'upload', - onClick: this.equipmentActions.import, - }, - { - text: this.intl.t('common.export'), - icon: 'long-arrow-up', - iconClass: 'rotate-icon-45', - wrapperClass: 'hidden md:flex', - onClick: this.equipmentActions.export, - }, + { icon: 'refresh', onClick: this.equipmentActions.refresh, helpText: this.intl.t('common.refresh') }, + { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.equipmentActions.transition.create }, + { text: this.intl.t('common.export'), icon: 'long-arrow-up', iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.equipmentActions.export }, ]; - /** bulk action buttons */ - @tracked bulkActions = [ - { - label: 'Delete selected...', - class: 'text-red-500', - fn: this.equipmentActions.bulkDelete, - }, - ]; + @tracked bulkActions = [{ label: 'Delete selected...', class: 'text-red-500', fn: this.equipmentActions.bulkDelete }]; - /** columns */ @tracked columns = [ - { - label: this.intl.t('column.name'), - valuePath: 'name', - - cellComponent: 'table/cell/anchor', - cellClassNames: 'uppercase', - action: this.equipmentActions.transition.view, - permission: 'fleet-ops view equipment', - hidden: true, - resizable: true, - sortable: true, - filterable: true, - filterParam: 'name', - filterComponent: 'filter/string', - }, - { - label: this.intl.t('column.created-at'), - valuePath: 'createdAt', - sortParam: 'created_at', - - resizable: true, - sortable: true, - filterable: true, - filterComponent: 'filter/date', - }, - { - label: this.intl.t('column.updated-at'), - valuePath: 'updatedAt', - sortParam: 'updated_at', - - resizable: true, - sortable: true, - hidden: true, - filterable: true, - filterComponent: 'filter/date', - }, - + { label: this.intl.t('column.name'), valuePath: 'name', cellComponent: 'table/cell/anchor', cellClassNames: 'uppercase', action: this.equipmentActions.transition.view, permission: 'fleet-ops view equipment', resizable: true, sortable: true, filterable: true, filterParam: 'name', filterComponent: 'filter/string' }, + { label: this.intl.t('column.code'), valuePath: 'code', resizable: true, sortable: true, filterable: true, filterParam: 'code', filterComponent: 'filter/string' }, + { label: this.intl.t('column.type'), valuePath: 'type', cellComponent: 'table/cell/humanize', resizable: true, sortable: true, filterable: true, filterParam: 'type', filterComponent: 'filter/string' }, + { label: this.intl.t('column.status'), valuePath: 'status', cellComponent: 'table/cell/status', resizable: true, sortable: true, filterable: true, filterParam: 'status', filterComponent: 'filter/string' }, + { label: this.intl.t('column.serial-number'), valuePath: 'serial_number', resizable: true, sortable: true }, + { label: this.intl.t('column.manufacturer'), valuePath: 'manufacturer', resizable: true, sortable: true }, + { label: this.intl.t('column.created-at'), valuePath: 'createdAt', sortParam: 'created_at', resizable: true, sortable: true, filterable: true, filterComponent: 'filter/date' }, + { label: this.intl.t('column.updated-at'), valuePath: 'updatedAt', sortParam: 'updated_at', resizable: true, sortable: true, hidden: true, filterable: true, filterComponent: 'filter/date' }, { label: '', cellComponent: 'table/cell/dropdown', @@ -99,31 +40,13 @@ export default class MaintenanceEquipmentIndexController extends Controller { ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.equipment') }), cellClassNames: 'overflow-visible', wrapperClass: 'flex items-center justify-end mx-2', - actions: [ - { - label: this.intl.t('column.view-details'), - fn: this.equipmentActions.transition.view, - permission: 'fleet-ops view equipment', - }, - { - label: this.intl.t('column.edit-place'), - fn: this.equipmentActions.transition.edit, - permission: 'fleet-ops update equipment', - }, - { - separator: true, - }, - { - label: this.intl.t('column.delete'), - fn: this.equipmentActions.delete, - permission: 'fleet-ops delete equipment', - }, + { label: this.intl.t('column.view-details'), fn: this.equipmentActions.transition.view, permission: 'fleet-ops view equipment' }, + { label: this.intl.t('column.edit-place'), fn: this.equipmentActions.transition.edit, permission: 'fleet-ops update equipment' }, + { separator: true }, + { label: this.intl.t('column.delete'), fn: this.equipmentActions.delete, permission: 'fleet-ops delete equipment' }, ], - sortable: false, - filterable: false, - resizable: false, - searchable: false, + sortable: false, filterable: false, resizable: false, searchable: false, }, ]; } diff --git a/addon/controllers/maintenance/equipment/index/details.js b/addon/controllers/maintenance/equipment/index/details.js index 45c19c2bc..167bd2aef 100644 --- a/addon/controllers/maintenance/equipment/index/details.js +++ b/addon/controllers/maintenance/equipment/index/details.js @@ -1,3 +1,41 @@ import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { isArray } from '@ember/array'; -export default class MaintenanceEquipmentIndexDetailsController extends Controller {} +export default class MaintenanceEquipmentIndexDetailsController extends Controller { + @service equipmentActions; + @service hostRouter; + @service intl; + @service menuService; + + @tracked overlay; + + get tabs() { + const registeredTabs = this.menuService.getMenuItems('fleet-ops:component:equipment:details'); + return [ + { route: 'console.fleet-ops.maintenance.equipment.index.details.index', label: this.intl.t('common.overview') }, + ...(isArray(registeredTabs) ? registeredTabs : []), + ]; + } + + get actionButtons() { + return [ + { icon: 'edit', fn: this.edit, permission: 'fleet-ops update equipment' }, + { icon: 'trash', fn: this.delete, permission: 'fleet-ops delete equipment' }, + ]; + } + + @action edit() { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index.edit', this.model); + } + + @action delete() { + return this.equipmentActions.delete(this.model, { + onConfirm: () => { + this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); + }, + }); + } +} diff --git a/addon/controllers/maintenance/equipment/index/details/index.js b/addon/controllers/maintenance/equipment/index/details/index.js index 08182e978..261c84db6 100644 --- a/addon/controllers/maintenance/equipment/index/details/index.js +++ b/addon/controllers/maintenance/equipment/index/details/index.js @@ -1,3 +1,2 @@ import Controller from '@ember/controller'; - export default class MaintenanceEquipmentIndexDetailsIndexController extends Controller {} diff --git a/addon/controllers/maintenance/equipment/index/edit.js b/addon/controllers/maintenance/equipment/index/edit.js index f8b829246..0f8318100 100644 --- a/addon/controllers/maintenance/equipment/index/edit.js +++ b/addon/controllers/maintenance/equipment/index/edit.js @@ -1,3 +1,58 @@ import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; -export default class MaintenanceEquipmentIndexEditController extends Controller {} +export default class MaintenanceEquipmentIndexEditController extends Controller { + @service hostRouter; + @service intl; + @service notifications; + @service modalsManager; + @service events; + + @tracked overlay; + + get actionButtons() { + return [{ icon: 'eye', fn: this.view }]; + } + + @task *save(equipment) { + try { + yield equipment.save(); + this.events.trackResourceUpdated(equipment); + this.overlay?.close(); + yield this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index.details', equipment); + this.notifications.success(this.intl.t('common.resource-updated-success', { resource: this.intl.t('resource.equipment'), resourceName: equipment.name })); + } catch (err) { + this.notifications.serverError(err); + } + } + + @action cancel() { + if (this.model.hasDirtyAttributes) { + return this.#confirmContinueWithUnsavedChanges(this.model); + } + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); + } + + @action view() { + if (this.model.hasDirtyAttributes) { + return this.#confirmContinueWithUnsavedChanges(this.model); + } + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index.details', this.model); + } + + #confirmContinueWithUnsavedChanges(equipment, options = {}) { + return this.modalsManager.confirm({ + title: this.intl.t('common.continue-without-saving'), + body: this.intl.t('common.continue-without-saving-prompt', { resource: this.intl.t('resource.equipment') }), + acceptButtonText: this.intl.t('common.continue'), + confirm: async () => { + equipment.rollbackAttributes(); + await this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index.details', equipment); + }, + ...options, + }); + } +} diff --git a/addon/controllers/maintenance/equipment/index/new.js b/addon/controllers/maintenance/equipment/index/new.js index ac77a44fa..132a288d4 100644 --- a/addon/controllers/maintenance/equipment/index/new.js +++ b/addon/controllers/maintenance/equipment/index/new.js @@ -1,3 +1,34 @@ import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; -export default class MaintenanceEquipmentIndexNewController extends Controller {} +export default class MaintenanceEquipmentIndexNewController extends Controller { + @service equipmentActions; + @service hostRouter; + @service intl; + @service notifications; + @service events; + + @tracked overlay; + @tracked equipment = this.equipmentActions.createNewInstance(); + + @task *save(equipment) { + try { + yield equipment.save(); + this.events.trackResourceCreated(equipment); + this.overlay?.close(); + yield this.hostRouter.refresh(); + yield this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index.details', equipment); + this.notifications.success(this.intl.t('common.resource-created-success', { resource: this.intl.t('resource.equipment') })); + this.resetForm(); + } catch (err) { + this.notifications.serverError(err); + } + } + + @action resetForm() { + this.equipment = this.equipmentActions.createNewInstance(); + } +} diff --git a/addon/controllers/maintenance/maintenances/index.js b/addon/controllers/maintenance/maintenances/index.js new file mode 100644 index 000000000..b62713559 --- /dev/null +++ b/addon/controllers/maintenance/maintenances/index.js @@ -0,0 +1,165 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; + +export default class MaintenanceMaintenancesIndexController extends Controller { + @service maintenanceActions; + @service intl; + + /** query params */ + @tracked queryParams = ['type', 'status', 'priority', 'page', 'limit', 'sort', 'query', 'public_id', 'created_at', 'updated_at']; + @tracked page = 1; + @tracked limit; + @tracked sort = '-created_at'; + @tracked public_id; + @tracked type; + @tracked status; + @tracked priority; + + /** action buttons */ + @tracked actionButtons = [ + { + icon: 'refresh', + onClick: this.maintenanceActions.refresh, + helpText: this.intl.t('common.refresh'), + }, + { + text: this.intl.t('common.new'), + type: 'primary', + icon: 'plus', + onClick: this.maintenanceActions.transition.create, + }, + { + text: this.intl.t('common.export'), + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + wrapperClass: 'hidden md:flex', + onClick: this.maintenanceActions.export, + }, + ]; + + /** bulk action buttons */ + @tracked bulkActions = [ + { + label: 'Delete selected...', + class: 'text-red-500', + fn: this.maintenanceActions.bulkDelete, + }, + ]; + + /** columns */ + @tracked columns = [ + { + label: this.intl.t('column.summary'), + valuePath: 'summary', + cellComponent: 'table/cell/anchor', + cellClassNames: 'uppercase', + action: this.maintenanceActions.transition.view, + permission: 'fleet-ops view maintenance', + resizable: true, + sortable: true, + filterable: true, + filterParam: 'summary', + filterComponent: 'filter/string', + }, + { + label: this.intl.t('column.type'), + valuePath: 'type', + cellComponent: 'table/cell/humanize', + resizable: true, + sortable: true, + filterable: true, + filterParam: 'type', + filterComponent: 'filter/string', + }, + { + label: this.intl.t('column.status'), + valuePath: 'status', + cellComponent: 'table/cell/status', + resizable: true, + sortable: true, + filterable: true, + filterParam: 'status', + filterComponent: 'filter/string', + }, + { + label: this.intl.t('column.priority'), + valuePath: 'priority', + cellComponent: 'table/cell/humanize', + resizable: true, + sortable: true, + filterable: true, + filterParam: 'priority', + filterComponent: 'filter/string', + }, + { + label: this.intl.t('column.scheduled-at'), + valuePath: 'scheduledAt', + sortParam: 'scheduled_at', + resizable: true, + sortable: true, + filterable: true, + filterComponent: 'filter/date', + }, + { + label: this.intl.t('column.total-cost'), + valuePath: 'total_cost', + cellComponent: 'table/cell/currency', + resizable: true, + sortable: true, + }, + { + label: this.intl.t('column.created-at'), + valuePath: 'createdAt', + sortParam: 'created_at', + resizable: true, + sortable: true, + filterable: true, + filterComponent: 'filter/date', + }, + { + label: this.intl.t('column.updated-at'), + valuePath: 'updatedAt', + sortParam: 'updated_at', + resizable: true, + sortable: true, + hidden: true, + filterable: true, + filterComponent: 'filter/date', + }, + { + label: '', + cellComponent: 'table/cell/dropdown', + ddButtonText: false, + ddButtonIcon: 'ellipsis-h', + ddButtonIconPrefix: 'fas', + ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.maintenance') }), + cellClassNames: 'overflow-visible', + wrapperClass: 'flex items-center justify-end mx-2', + actions: [ + { + label: this.intl.t('column.view-details'), + fn: this.maintenanceActions.transition.view, + permission: 'fleet-ops view maintenance', + }, + { + label: this.intl.t('column.edit-place'), + fn: this.maintenanceActions.transition.edit, + permission: 'fleet-ops update maintenance', + }, + { + separator: true, + }, + { + label: this.intl.t('column.delete'), + fn: this.maintenanceActions.delete, + permission: 'fleet-ops delete maintenance', + }, + ], + sortable: false, + filterable: false, + resizable: false, + searchable: false, + }, + ]; +} diff --git a/addon/controllers/maintenance/maintenances/index/details.js b/addon/controllers/maintenance/maintenances/index/details.js new file mode 100644 index 000000000..3476772b8 --- /dev/null +++ b/addon/controllers/maintenance/maintenances/index/details.js @@ -0,0 +1,53 @@ +import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { isArray } from '@ember/array'; + +export default class MaintenanceMaintenancesIndexDetailsController extends Controller { + @service maintenanceActions; + @service hostRouter; + @service intl; + @service abilities; + @service menuService; + + @tracked overlay; + + get tabs() { + const registeredTabs = this.menuService.getMenuItems('fleet-ops:component:maintenance:details'); + return [ + { + route: 'console.fleet-ops.maintenance.maintenances.index.details.index', + label: this.intl.t('common.overview'), + }, + ...(isArray(registeredTabs) ? registeredTabs : []), + ]; + } + + get actionButtons() { + return [ + { + icon: 'edit', + fn: this.edit, + permission: 'fleet-ops update maintenance', + }, + { + icon: 'trash', + fn: this.delete, + permission: 'fleet-ops delete maintenance', + }, + ]; + } + + @action edit() { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index.edit', this.model); + } + + @action delete() { + return this.maintenanceActions.delete(this.model, { + onConfirm: () => { + this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); + }, + }); + } +} diff --git a/addon/controllers/maintenance/maintenances/index/details/index.js b/addon/controllers/maintenance/maintenances/index/details/index.js new file mode 100644 index 000000000..37e6166e8 --- /dev/null +++ b/addon/controllers/maintenance/maintenances/index/details/index.js @@ -0,0 +1,3 @@ +import Controller from '@ember/controller'; + +export default class MaintenanceMaintenancesIndexDetailsIndexController extends Controller {} diff --git a/addon/controllers/maintenance/maintenances/index/edit.js b/addon/controllers/maintenance/maintenances/index/edit.js new file mode 100644 index 000000000..730b09d62 --- /dev/null +++ b/addon/controllers/maintenance/maintenances/index/edit.js @@ -0,0 +1,68 @@ +import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; + +export default class MaintenanceMaintenancesIndexEditController extends Controller { + @service hostRouter; + @service intl; + @service notifications; + @service modalsManager; + @service events; + + @tracked overlay; + + get actionButtons() { + return [ + { + icon: 'eye', + fn: this.view, + }, + ]; + } + + @task *save(maintenance) { + try { + yield maintenance.save(); + this.events.trackResourceUpdated(maintenance); + this.overlay?.close(); + yield this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index.details', maintenance); + this.notifications.success( + this.intl.t('common.resource-updated-success', { + resource: this.intl.t('resource.maintenance'), + resourceName: maintenance.summary, + }) + ); + } catch (err) { + this.notifications.serverError(err); + } + } + + @action cancel() { + if (this.model.hasDirtyAttributes) { + return this.#confirmContinueWithUnsavedChanges(this.model); + } + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); + } + + @action view() { + if (this.model.hasDirtyAttributes) { + return this.#confirmContinueWithUnsavedChanges(this.model); + } + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index.details', this.model); + } + + #confirmContinueWithUnsavedChanges(maintenance, options = {}) { + return this.modalsManager.confirm({ + title: this.intl.t('common.continue-without-saving'), + body: this.intl.t('common.continue-without-saving-prompt', { resource: this.intl.t('resource.maintenance') }), + acceptButtonText: this.intl.t('common.continue'), + confirm: async () => { + maintenance.rollbackAttributes(); + await this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index.details', maintenance); + }, + ...options, + }); + } +} diff --git a/addon/controllers/maintenance/maintenances/index/new.js b/addon/controllers/maintenance/maintenances/index/new.js new file mode 100644 index 000000000..d2c773111 --- /dev/null +++ b/addon/controllers/maintenance/maintenances/index/new.js @@ -0,0 +1,34 @@ +import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; + +export default class MaintenanceMaintenancesIndexNewController extends Controller { + @service maintenanceActions; + @service hostRouter; + @service intl; + @service notifications; + @service events; + + @tracked overlay; + @tracked maintenance = this.maintenanceActions.createNewInstance(); + + @task *save(maintenance) { + try { + yield maintenance.save(); + this.events.trackResourceCreated(maintenance); + this.overlay?.close(); + yield this.hostRouter.refresh(); + yield this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index.details', maintenance); + this.notifications.success(this.intl.t('common.resource-created-success', { resource: this.intl.t('resource.maintenance') })); + this.resetForm(); + } catch (err) { + this.notifications.serverError(err); + } + } + + @action resetForm() { + this.maintenance = this.maintenanceActions.createNewInstance(); + } +} diff --git a/addon/controllers/maintenance/parts/index.js b/addon/controllers/maintenance/parts/index.js index 52d7716ac..73ea4d787 100644 --- a/addon/controllers/maintenance/parts/index.js +++ b/addon/controllers/maintenance/parts/index.js @@ -6,90 +6,31 @@ export default class MaintenancePartsIndexController extends Controller { @service partActions; @service intl; - /** query params */ - @tracked queryParams = ['name', 'page', 'limit', 'sort', 'query', 'public_id', 'created_at', 'updated_at']; + @tracked queryParams = ['type', 'status', 'page', 'limit', 'sort', 'query', 'public_id', 'created_at', 'updated_at']; @tracked page = 1; @tracked limit; @tracked sort = '-created_at'; @tracked public_id; - @tracked name; + @tracked type; + @tracked status; - /** action buttons */ @tracked actionButtons = [ - { - icon: 'refresh', - onClick: this.partActions.refresh, - helpText: this.intl.t('common.refresh'), - }, - { - text: this.intl.t('common.new'), - type: 'primary', - icon: 'plus', - onClick: this.partActions.transition.create, - }, - { - text: this.intl.t('common.import'), - type: 'magic', - icon: 'upload', - onClick: this.partActions.import, - }, - { - text: this.intl.t('common.export'), - icon: 'long-arrow-up', - iconClass: 'rotate-icon-45', - wrapperClass: 'hidden md:flex', - onClick: this.partActions.export, - }, + { icon: 'refresh', onClick: this.partActions.refresh, helpText: this.intl.t('common.refresh') }, + { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.partActions.transition.create }, + { text: this.intl.t('common.export'), icon: 'long-arrow-up', iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.partActions.export }, ]; - /** bulk action buttons */ - @tracked bulkActions = [ - { - label: 'Delete selected...', - class: 'text-red-500', - fn: this.partActions.bulkDelete, - }, - ]; + @tracked bulkActions = [{ label: 'Delete selected...', class: 'text-red-500', fn: this.partActions.bulkDelete }]; - /** columns */ @tracked columns = [ - { - label: this.intl.t('column.name'), - valuePath: 'name', - - cellComponent: 'table/cell/anchor', - cellClassNames: 'uppercase', - action: this.partActions.transition.view, - permission: 'fleet-ops view part', - hidden: true, - resizable: true, - sortable: true, - filterable: true, - filterParam: 'name', - filterComponent: 'filter/string', - }, - { - label: this.intl.t('column.created-at'), - valuePath: 'createdAt', - sortParam: 'created_at', - - resizable: true, - sortable: true, - filterable: true, - filterComponent: 'filter/date', - }, - { - label: this.intl.t('column.updated-at'), - valuePath: 'updatedAt', - sortParam: 'updated_at', - - resizable: true, - sortable: true, - hidden: true, - filterable: true, - filterComponent: 'filter/date', - }, - + { label: this.intl.t('column.name'), valuePath: 'name', cellComponent: 'table/cell/anchor', cellClassNames: 'uppercase', action: this.partActions.transition.view, permission: 'fleet-ops view part', resizable: true, sortable: true, filterable: true, filterParam: 'name', filterComponent: 'filter/string' }, + { label: this.intl.t('column.part-number'), valuePath: 'part_number', resizable: true, sortable: true, filterable: true, filterParam: 'part_number', filterComponent: 'filter/string' }, + { label: this.intl.t('column.type'), valuePath: 'type', cellComponent: 'table/cell/humanize', resizable: true, sortable: true, filterable: true, filterParam: 'type', filterComponent: 'filter/string' }, + { label: this.intl.t('column.status'), valuePath: 'status', cellComponent: 'table/cell/status', resizable: true, sortable: true, filterable: true, filterParam: 'status', filterComponent: 'filter/string' }, + { label: this.intl.t('column.quantity-on-hand'), valuePath: 'quantity_on_hand', resizable: true, sortable: true }, + { label: this.intl.t('column.unit-cost'), valuePath: 'unit_cost', cellComponent: 'table/cell/currency', resizable: true, sortable: true }, + { label: this.intl.t('column.created-at'), valuePath: 'createdAt', sortParam: 'created_at', resizable: true, sortable: true, filterable: true, filterComponent: 'filter/date' }, + { label: this.intl.t('column.updated-at'), valuePath: 'updatedAt', sortParam: 'updated_at', resizable: true, sortable: true, hidden: true, filterable: true, filterComponent: 'filter/date' }, { label: '', cellComponent: 'table/cell/dropdown', @@ -99,31 +40,13 @@ export default class MaintenancePartsIndexController extends Controller { ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.part') }), cellClassNames: 'overflow-visible', wrapperClass: 'flex items-center justify-end mx-2', - actions: [ - { - label: this.intl.t('column.view-details'), - fn: this.partActions.transition.view, - permission: 'fleet-ops view part', - }, - { - label: this.intl.t('column.edit-place'), - fn: this.partActions.transition.edit, - permission: 'fleet-ops update part', - }, - { - separator: true, - }, - { - label: this.intl.t('column.delete'), - fn: this.partActions.delete, - permission: 'fleet-ops delete part', - }, + { label: this.intl.t('column.view-details'), fn: this.partActions.transition.view, permission: 'fleet-ops view part' }, + { label: this.intl.t('column.edit-place'), fn: this.partActions.transition.edit, permission: 'fleet-ops update part' }, + { separator: true }, + { label: this.intl.t('column.delete'), fn: this.partActions.delete, permission: 'fleet-ops delete part' }, ], - sortable: false, - filterable: false, - resizable: false, - searchable: false, + sortable: false, filterable: false, resizable: false, searchable: false, }, ]; } diff --git a/addon/controllers/maintenance/parts/index/details.js b/addon/controllers/maintenance/parts/index/details.js index 3212c9018..105091ec5 100644 --- a/addon/controllers/maintenance/parts/index/details.js +++ b/addon/controllers/maintenance/parts/index/details.js @@ -1,3 +1,41 @@ import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { isArray } from '@ember/array'; -export default class MaintenancePartsIndexDetailsController extends Controller {} +export default class MaintenancePartsIndexDetailsController extends Controller { + @service partActions; + @service hostRouter; + @service intl; + @service menuService; + + @tracked overlay; + + get tabs() { + const registeredTabs = this.menuService.getMenuItems('fleet-ops:component:part:details'); + return [ + { route: 'console.fleet-ops.maintenance.parts.index.details.index', label: this.intl.t('common.overview') }, + ...(isArray(registeredTabs) ? registeredTabs : []), + ]; + } + + get actionButtons() { + return [ + { icon: 'edit', fn: this.edit, permission: 'fleet-ops update part' }, + { icon: 'trash', fn: this.delete, permission: 'fleet-ops delete part' }, + ]; + } + + @action edit() { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index.edit', this.model); + } + + @action delete() { + return this.partActions.delete(this.model, { + onConfirm: () => { + this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); + }, + }); + } +} diff --git a/addon/controllers/maintenance/parts/index/details/index.js b/addon/controllers/maintenance/parts/index/details/index.js index 1f3db998f..e82cd2f4d 100644 --- a/addon/controllers/maintenance/parts/index/details/index.js +++ b/addon/controllers/maintenance/parts/index/details/index.js @@ -1,3 +1,2 @@ import Controller from '@ember/controller'; - export default class MaintenancePartsIndexDetailsIndexController extends Controller {} diff --git a/addon/controllers/maintenance/parts/index/edit.js b/addon/controllers/maintenance/parts/index/edit.js index 544d4c677..02624ef11 100644 --- a/addon/controllers/maintenance/parts/index/edit.js +++ b/addon/controllers/maintenance/parts/index/edit.js @@ -1,3 +1,58 @@ import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; -export default class MaintenancePartsIndexEditController extends Controller {} +export default class MaintenancePartsIndexEditController extends Controller { + @service hostRouter; + @service intl; + @service notifications; + @service modalsManager; + @service events; + + @tracked overlay; + + get actionButtons() { + return [{ icon: 'eye', fn: this.view }]; + } + + @task *save(part) { + try { + yield part.save(); + this.events.trackResourceUpdated(part); + this.overlay?.close(); + yield this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index.details', part); + this.notifications.success(this.intl.t('common.resource-updated-success', { resource: this.intl.t('resource.part'), resourceName: part.name })); + } catch (err) { + this.notifications.serverError(err); + } + } + + @action cancel() { + if (this.model.hasDirtyAttributes) { + return this.#confirmContinueWithUnsavedChanges(this.model); + } + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); + } + + @action view() { + if (this.model.hasDirtyAttributes) { + return this.#confirmContinueWithUnsavedChanges(this.model); + } + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index.details', this.model); + } + + #confirmContinueWithUnsavedChanges(part, options = {}) { + return this.modalsManager.confirm({ + title: this.intl.t('common.continue-without-saving'), + body: this.intl.t('common.continue-without-saving-prompt', { resource: this.intl.t('resource.part') }), + acceptButtonText: this.intl.t('common.continue'), + confirm: async () => { + part.rollbackAttributes(); + await this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index.details', part); + }, + ...options, + }); + } +} diff --git a/addon/controllers/maintenance/parts/index/new.js b/addon/controllers/maintenance/parts/index/new.js index b44fb2496..45c126681 100644 --- a/addon/controllers/maintenance/parts/index/new.js +++ b/addon/controllers/maintenance/parts/index/new.js @@ -1,3 +1,34 @@ import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; -export default class MaintenancePartsIndexNewController extends Controller {} +export default class MaintenancePartsIndexNewController extends Controller { + @service partActions; + @service hostRouter; + @service intl; + @service notifications; + @service events; + + @tracked overlay; + @tracked part = this.partActions.createNewInstance(); + + @task *save(part) { + try { + yield part.save(); + this.events.trackResourceCreated(part); + this.overlay?.close(); + yield this.hostRouter.refresh(); + yield this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index.details', part); + this.notifications.success(this.intl.t('common.resource-created-success', { resource: this.intl.t('resource.part') })); + this.resetForm(); + } catch (err) { + this.notifications.serverError(err); + } + } + + @action resetForm() { + this.part = this.partActions.createNewInstance(); + } +} diff --git a/addon/controllers/maintenance/work-orders/index.js b/addon/controllers/maintenance/work-orders/index.js index 7d45f37ae..20aeb8ec6 100644 --- a/addon/controllers/maintenance/work-orders/index.js +++ b/addon/controllers/maintenance/work-orders/index.js @@ -6,119 +6,45 @@ export default class MaintenanceWorkOrdersIndexController extends Controller { @service workOrderActions; @service intl; - /** query params */ - @tracked queryParams = ['name', 'page', 'limit', 'sort', 'query', 'public_id', 'created_at', 'updated_at']; + @tracked queryParams = ['status', 'priority', 'page', 'limit', 'sort', 'query', 'public_id', 'created_at', 'updated_at']; @tracked page = 1; @tracked limit; @tracked sort = '-created_at'; @tracked public_id; - @tracked name; + @tracked status; + @tracked priority; - /** action buttons */ @tracked actionButtons = [ - { - icon: 'refresh', - onClick: this.workOrderActions.refresh, - helpText: this.intl.t('common.refresh'), - }, - { - text: this.intl.t('common.new'), - type: 'primary', - icon: 'plus', - onClick: this.workOrderActions.transition.create, - }, - { - text: this.intl.t('common.import'), - type: 'magic', - icon: 'upload', - onClick: this.workOrderActions.import, - }, - { - text: this.intl.t('common.export'), - icon: 'long-arrow-up', - iconClass: 'rotate-icon-45', - wrapperClass: 'hidden md:flex', - onClick: this.workOrderActions.export, - }, + { icon: 'refresh', onClick: this.workOrderActions.refresh, helpText: this.intl.t('common.refresh') }, + { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.workOrderActions.transition.create }, + { text: this.intl.t('common.export'), icon: 'long-arrow-up', iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.workOrderActions.export }, ]; - /** bulk action buttons */ - @tracked bulkActions = [ - { - label: 'Delete selected...', - class: 'text-red-500', - fn: this.workOrderActions.bulkDelete, - }, - ]; + @tracked bulkActions = [{ label: 'Delete selected...', class: 'text-red-500', fn: this.workOrderActions.bulkDelete }]; - /** columns */ @tracked columns = [ - { - label: this.intl.t('column.name'), - valuePath: 'name', - - cellComponent: 'table/cell/anchor', - cellClassNames: 'uppercase', - action: this.workOrderActions.transition.view, - permission: 'fleet-ops view work-order', - hidden: true, - resizable: true, - sortable: true, - filterable: true, - filterParam: 'name', - filterComponent: 'filter/string', - }, - { - label: this.intl.t('column.created-at'), - valuePath: 'createdAt', - sortParam: 'created_at', - - resizable: true, - sortable: true, - filterable: true, - filterComponent: 'filter/date', - }, - { - label: this.intl.t('column.updated-at'), - valuePath: 'updatedAt', - sortParam: 'updated_at', - - resizable: true, - sortable: true, - hidden: true, - filterable: true, - filterComponent: 'filter/date', - }, - + { label: this.intl.t('column.code'), valuePath: 'code', cellComponent: 'table/cell/anchor', cellClassNames: 'uppercase', action: this.workOrderActions.transition.view, permission: 'fleet-ops view work-order', resizable: true, sortable: true, filterable: true, filterParam: 'code', filterComponent: 'filter/string' }, + { label: this.intl.t('column.subject'), valuePath: 'subject', resizable: true, sortable: true, filterable: true, filterParam: 'subject', filterComponent: 'filter/string' }, + { label: this.intl.t('column.status'), valuePath: 'status', cellComponent: 'table/cell/status', resizable: true, sortable: true, filterable: true, filterParam: 'status', filterComponent: 'filter/string' }, + { label: this.intl.t('column.priority'), valuePath: 'priority', cellComponent: 'table/cell/humanize', resizable: true, sortable: true, filterable: true, filterParam: 'priority', filterComponent: 'filter/string' }, + { label: this.intl.t('column.assignee'), valuePath: 'assignee_name', resizable: true, sortable: false }, + { label: this.intl.t('column.due-at'), valuePath: 'dueAt', sortParam: 'due_at', resizable: true, sortable: true, filterable: true, filterComponent: 'filter/date' }, + { label: this.intl.t('column.created-at'), valuePath: 'createdAt', sortParam: 'created_at', resizable: true, sortable: true, filterable: true, filterComponent: 'filter/date' }, + { label: this.intl.t('column.updated-at'), valuePath: 'updatedAt', sortParam: 'updated_at', resizable: true, sortable: true, hidden: true, filterable: true, filterComponent: 'filter/date' }, { label: '', cellComponent: 'table/cell/dropdown', ddButtonText: false, ddButtonIcon: 'ellipsis-h', ddButtonIconPrefix: 'fas', - ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.Work Order') }), + ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.work-order') }), cellClassNames: 'overflow-visible', wrapperClass: 'flex items-center justify-end mx-2', - actions: [ - { - label: this.intl.t('column.view-details'), - fn: this.workOrderActions.transition.view, - permission: 'fleet-ops view work-order', - }, - { - label: this.intl.t('column.edit-place'), - fn: this.workOrderActions.transition.edit, - permission: 'fleet-ops update work-order', - }, - { - separator: true, - }, - { - label: this.intl.t('column.delete'), - fn: this.workOrderActions.delete, - permission: 'fleet-ops delete work-order', - }, + { label: this.intl.t('column.view-details'), fn: this.workOrderActions.transition.view, permission: 'fleet-ops view work-order' }, + { label: this.intl.t('column.edit-place'), fn: this.workOrderActions.transition.edit, permission: 'fleet-ops update work-order' }, + { separator: true }, + { label: this.intl.t('column.delete'), fn: this.workOrderActions.delete, permission: 'fleet-ops delete work-order' }, ], sortable: false, filterable: false, diff --git a/addon/controllers/maintenance/work-orders/index/details.js b/addon/controllers/maintenance/work-orders/index/details.js index a28431126..aa12fd398 100644 --- a/addon/controllers/maintenance/work-orders/index/details.js +++ b/addon/controllers/maintenance/work-orders/index/details.js @@ -1,3 +1,42 @@ import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { isArray } from '@ember/array'; -export default class MaintenanceWorkOrdersIndexDetailsController extends Controller {} +export default class MaintenanceWorkOrdersIndexDetailsController extends Controller { + @service workOrderActions; + @service hostRouter; + @service intl; + @service abilities; + @service menuService; + + @tracked overlay; + + get tabs() { + const registeredTabs = this.menuService.getMenuItems('fleet-ops:component:work-order:details'); + return [ + { route: 'console.fleet-ops.maintenance.work-orders.index.details.index', label: this.intl.t('common.overview') }, + ...(isArray(registeredTabs) ? registeredTabs : []), + ]; + } + + get actionButtons() { + return [ + { icon: 'edit', fn: this.edit, permission: 'fleet-ops update work-order' }, + { icon: 'trash', fn: this.delete, permission: 'fleet-ops delete work-order' }, + ]; + } + + @action edit() { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index.edit', this.model); + } + + @action delete() { + return this.workOrderActions.delete(this.model, { + onConfirm: () => { + this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); + }, + }); + } +} diff --git a/addon/controllers/maintenance/work-orders/index/edit.js b/addon/controllers/maintenance/work-orders/index/edit.js index 768242f27..6f6c75a5c 100644 --- a/addon/controllers/maintenance/work-orders/index/edit.js +++ b/addon/controllers/maintenance/work-orders/index/edit.js @@ -1,3 +1,58 @@ import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; -export default class MaintenanceWorkOrdersIndexEditController extends Controller {} +export default class MaintenanceWorkOrdersIndexEditController extends Controller { + @service hostRouter; + @service intl; + @service notifications; + @service modalsManager; + @service events; + + @tracked overlay; + + get actionButtons() { + return [{ icon: 'eye', fn: this.view }]; + } + + @task *save(workOrder) { + try { + yield workOrder.save(); + this.events.trackResourceUpdated(workOrder); + this.overlay?.close(); + yield this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index.details', workOrder); + this.notifications.success(this.intl.t('common.resource-updated-success', { resource: this.intl.t('resource.work-order'), resourceName: workOrder.code })); + } catch (err) { + this.notifications.serverError(err); + } + } + + @action cancel() { + if (this.model.hasDirtyAttributes) { + return this.#confirmContinueWithUnsavedChanges(this.model); + } + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); + } + + @action view() { + if (this.model.hasDirtyAttributes) { + return this.#confirmContinueWithUnsavedChanges(this.model); + } + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index.details', this.model); + } + + #confirmContinueWithUnsavedChanges(workOrder, options = {}) { + return this.modalsManager.confirm({ + title: this.intl.t('common.continue-without-saving'), + body: this.intl.t('common.continue-without-saving-prompt', { resource: this.intl.t('resource.work-order') }), + acceptButtonText: this.intl.t('common.continue'), + confirm: async () => { + workOrder.rollbackAttributes(); + await this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index.details', workOrder); + }, + ...options, + }); + } +} diff --git a/addon/controllers/maintenance/work-orders/index/new.js b/addon/controllers/maintenance/work-orders/index/new.js index 6594b68d2..7936e5891 100644 --- a/addon/controllers/maintenance/work-orders/index/new.js +++ b/addon/controllers/maintenance/work-orders/index/new.js @@ -1,3 +1,34 @@ import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; -export default class MaintenanceWorkOrdersIndexNewController extends Controller {} +export default class MaintenanceWorkOrdersIndexNewController extends Controller { + @service workOrderActions; + @service hostRouter; + @service intl; + @service notifications; + @service events; + + @tracked overlay; + @tracked workOrder = this.workOrderActions.createNewInstance(); + + @task *save(workOrder) { + try { + yield workOrder.save(); + this.events.trackResourceCreated(workOrder); + this.overlay?.close(); + yield this.hostRouter.refresh(); + yield this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index.details', workOrder); + this.notifications.success(this.intl.t('common.resource-created-success', { resource: this.intl.t('resource.work-order') })); + this.resetForm(); + } catch (err) { + this.notifications.serverError(err); + } + } + + @action resetForm() { + this.workOrder = this.workOrderActions.createNewInstance(); + } +} diff --git a/addon/extension.js b/addon/extension.js index 77da0f770..5d6400b41 100644 --- a/addon/extension.js +++ b/addon/extension.js @@ -153,6 +153,18 @@ export default { 'fleet-ops:component:issue:form:details', 'fleet-ops:component:fuel-report:form', 'fleet-ops:component:fuel-report:form:details', + 'fleet-ops:component:maintenance:form', + 'fleet-ops:component:maintenance:form:details', + 'fleet-ops:component:maintenance:details', + 'fleet-ops:component:work-order:form', + 'fleet-ops:component:work-order:form:details', + 'fleet-ops:component:work-order:details', + 'fleet-ops:component:equipment:form', + 'fleet-ops:component:equipment:form:details', + 'fleet-ops:component:equipment:details', + 'fleet-ops:component:part:form', + 'fleet-ops:component:part:form:details', + 'fleet-ops:component:part:details', 'fleet-ops:contextmenu:vehicle', 'fleet-ops:contextmenu:driver', 'fleet-ops:component:order:details', diff --git a/addon/routes.js b/addon/routes.js index ebeb8e18c..0cdfdb4ff 100644 --- a/addon/routes.js +++ b/addon/routes.js @@ -181,6 +181,16 @@ export default buildRoutes(function () { this.route('tracking'); }); this.route('maintenance', function () { + this.route('maintenances', function () { + this.route('index', { path: '/' }, function () { + this.route('new'); + this.route('edit', { path: '/edit/:public_id' }); + this.route('details', { path: '/:public_id' }, function () { + this.route('index', { path: '/' }); + }); + }); + }); + this.route('work-orders', function () { this.route('index', { path: '/' }, function () { this.route('new'); diff --git a/addon/routes/maintenance/equipment/index/details.js b/addon/routes/maintenance/equipment/index/details.js index 2fa76ce25..7f8efa9b7 100644 --- a/addon/routes/maintenance/equipment/index/details.js +++ b/addon/routes/maintenance/equipment/index/details.js @@ -1,3 +1,29 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; -export default class MaintenanceEquipmentIndexDetailsRoute extends Route {} +export default class MaintenanceEquipmentIndexDetailsRoute extends Route { + @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + @action error(error) { + this.notifications.serverError(error); + if (typeof error.message === 'string' && error.message.endsWith('not found')) { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); + } + } + + beforeModel() { + if (this.abilities.cannot('fleet-ops view equipment')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); + } + } + + model({ public_id }) { + return this.store.findRecord('equipment', public_id); + } +} diff --git a/addon/routes/maintenance/equipment/index/edit.js b/addon/routes/maintenance/equipment/index/edit.js index 78ceda5c2..51e06996b 100644 --- a/addon/routes/maintenance/equipment/index/edit.js +++ b/addon/routes/maintenance/equipment/index/edit.js @@ -1,3 +1,29 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; -export default class MaintenanceEquipmentIndexEditRoute extends Route {} +export default class MaintenanceEquipmentIndexEditRoute extends Route { + @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + @action error(error) { + this.notifications.serverError(error); + if (typeof error.message === 'string' && error.message.endsWith('not found')) { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); + } + } + + beforeModel() { + if (this.abilities.cannot('fleet-ops update equipment')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); + } + } + + model({ public_id }) { + return this.store.findRecord('equipment', public_id); + } +} diff --git a/addon/routes/maintenance/maintenances.js b/addon/routes/maintenance/maintenances.js new file mode 100644 index 000000000..c38f607a0 --- /dev/null +++ b/addon/routes/maintenance/maintenances.js @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class MaintenanceMaintenancesRoute extends Route {} diff --git a/addon/routes/maintenance/maintenances/index.js b/addon/routes/maintenance/maintenances/index.js new file mode 100644 index 000000000..419452622 --- /dev/null +++ b/addon/routes/maintenance/maintenances/index.js @@ -0,0 +1,23 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class MaintenanceMaintenancesIndexRoute extends Route { + @service store; + + queryParams = { + page: { refreshModel: true }, + limit: { refreshModel: true }, + sort: { refreshModel: true }, + query: { refreshModel: true }, + type: { refreshModel: true }, + status: { refreshModel: true }, + priority: { refreshModel: true }, + public_id: { refreshModel: true }, + created_at: { refreshModel: true }, + updated_at: { refreshModel: true }, + }; + + model(params) { + return this.store.query('maintenance', { ...params }); + } +} diff --git a/addon/routes/maintenance/maintenances/index/details.js b/addon/routes/maintenance/maintenances/index/details.js new file mode 100644 index 000000000..8bdbd46bc --- /dev/null +++ b/addon/routes/maintenance/maintenances/index/details.js @@ -0,0 +1,29 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +export default class MaintenanceMaintenancesIndexDetailsRoute extends Route { + @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + @action error(error) { + this.notifications.serverError(error); + if (typeof error.message === 'string' && error.message.endsWith('not found')) { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); + } + } + + beforeModel() { + if (this.abilities.cannot('fleet-ops view maintenance')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); + } + } + + model({ public_id }) { + return this.store.findRecord('maintenance', public_id); + } +} diff --git a/addon/routes/maintenance/maintenances/index/details/index.js b/addon/routes/maintenance/maintenances/index/details/index.js new file mode 100644 index 000000000..785604f17 --- /dev/null +++ b/addon/routes/maintenance/maintenances/index/details/index.js @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class MaintenanceMaintenancesIndexDetailsIndexRoute extends Route {} diff --git a/addon/routes/maintenance/maintenances/index/edit.js b/addon/routes/maintenance/maintenances/index/edit.js new file mode 100644 index 000000000..ec801d142 --- /dev/null +++ b/addon/routes/maintenance/maintenances/index/edit.js @@ -0,0 +1,29 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +export default class MaintenanceMaintenancesIndexEditRoute extends Route { + @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + @action error(error) { + this.notifications.serverError(error); + if (typeof error.message === 'string' && error.message.endsWith('not found')) { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); + } + } + + beforeModel() { + if (this.abilities.cannot('fleet-ops update maintenance')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); + } + } + + model({ public_id }) { + return this.store.findRecord('maintenance', public_id); + } +} diff --git a/addon/routes/maintenance/maintenances/index/new.js b/addon/routes/maintenance/maintenances/index/new.js new file mode 100644 index 000000000..8bcba94e0 --- /dev/null +++ b/addon/routes/maintenance/maintenances/index/new.js @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class MaintenanceMaintenancesIndexNewRoute extends Route {} diff --git a/addon/routes/maintenance/parts/index/details.js b/addon/routes/maintenance/parts/index/details.js index 66f0aa974..5a311fb5c 100644 --- a/addon/routes/maintenance/parts/index/details.js +++ b/addon/routes/maintenance/parts/index/details.js @@ -1,3 +1,29 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; -export default class MaintenancePartsIndexDetailsRoute extends Route {} +export default class MaintenancePartsIndexDetailsRoute extends Route { + @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + @action error(error) { + this.notifications.serverError(error); + if (typeof error.message === 'string' && error.message.endsWith('not found')) { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); + } + } + + beforeModel() { + if (this.abilities.cannot('fleet-ops view part')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); + } + } + + model({ public_id }) { + return this.store.findRecord('part', public_id); + } +} diff --git a/addon/routes/maintenance/parts/index/edit.js b/addon/routes/maintenance/parts/index/edit.js index 23a6e7426..18c9c8bbb 100644 --- a/addon/routes/maintenance/parts/index/edit.js +++ b/addon/routes/maintenance/parts/index/edit.js @@ -1,3 +1,29 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; -export default class MaintenancePartsIndexEditRoute extends Route {} +export default class MaintenancePartsIndexEditRoute extends Route { + @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + @action error(error) { + this.notifications.serverError(error); + if (typeof error.message === 'string' && error.message.endsWith('not found')) { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); + } + } + + beforeModel() { + if (this.abilities.cannot('fleet-ops update part')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); + } + } + + model({ public_id }) { + return this.store.findRecord('part', public_id); + } +} diff --git a/addon/routes/maintenance/work-orders/index/details.js b/addon/routes/maintenance/work-orders/index/details.js index 6065dee3c..52738d3f2 100644 --- a/addon/routes/maintenance/work-orders/index/details.js +++ b/addon/routes/maintenance/work-orders/index/details.js @@ -1,3 +1,29 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; -export default class MaintenanceWorkOrdersIndexDetailsRoute extends Route {} +export default class MaintenanceWorkOrdersIndexDetailsRoute extends Route { + @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + @action error(error) { + this.notifications.serverError(error); + if (typeof error.message === 'string' && error.message.endsWith('not found')) { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); + } + } + + beforeModel() { + if (this.abilities.cannot('fleet-ops view work-order')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); + } + } + + model({ public_id }) { + return this.store.findRecord('work-order', public_id); + } +} diff --git a/addon/routes/maintenance/work-orders/index/edit.js b/addon/routes/maintenance/work-orders/index/edit.js index 25fac7a5d..1ccfe4bdd 100644 --- a/addon/routes/maintenance/work-orders/index/edit.js +++ b/addon/routes/maintenance/work-orders/index/edit.js @@ -1,3 +1,29 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; -export default class MaintenanceWorkOrdersIndexEditRoute extends Route {} +export default class MaintenanceWorkOrdersIndexEditRoute extends Route { + @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + @action error(error) { + this.notifications.serverError(error); + if (typeof error.message === 'string' && error.message.endsWith('not found')) { + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); + } + } + + beforeModel() { + if (this.abilities.cannot('fleet-ops update work-order')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); + } + } + + model({ public_id }) { + return this.store.findRecord('work-order', public_id); + } +} diff --git a/addon/services/maintenance-actions.js b/addon/services/maintenance-actions.js index f1f8c83ba..9235274b1 100644 --- a/addon/services/maintenance-actions.js +++ b/addon/services/maintenance-actions.js @@ -19,16 +19,14 @@ export default class MaintenanceActionsService extends ResourceActionService { content: 'maintenance/form', title: this.intl.t('common.create-a-new-resource', { resource: this.intl.t('resource.maintenance')?.toLowerCase() }), useDefaultSaveTask: true, - saveOptions: { - callback: this.refresh, - }, + saveOptions: { callback: this.refresh }, maintenance, }); }, edit: (maintenance) => { return this.resourceContextPanel.open({ content: 'maintenance/form', - title: this.intl.t('common.edit-resource-name', { resourceName: maintenance.name }), + title: this.intl.t('common.edit-resource-name', { resourceName: maintenance.summary }), useDefaultSaveTask: true, maintenance, }); @@ -36,12 +34,7 @@ export default class MaintenanceActionsService extends ResourceActionService { view: (maintenance) => { return this.resourceContextPanel.open({ maintenance, - tabs: [ - { - label: this.intl.t('common.overview'), - component: 'maintenance/details', - }, - ], + tabs: [{ label: this.intl.t('common.overview'), component: 'maintenance/details' }], }); }, }; @@ -61,7 +54,7 @@ export default class MaintenanceActionsService extends ResourceActionService { edit: (maintenance, options = {}, saveOptions = {}) => { return this.modalsManager.show('modals/resource', { resource: maintenance, - title: this.intl.t('common.edit-resource-name', { resourceName: maintenance.name }), + title: this.intl.t('common.edit-resource-name', { resourceName: maintenance.summary }), acceptButtonText: this.intl.t('common.save-changes'), saveButtonIcon: 'save', component: 'maintenance/form', @@ -72,7 +65,7 @@ export default class MaintenanceActionsService extends ResourceActionService { view: (maintenance, options = {}) => { return this.modalsManager.show('modals/resource', { resource: maintenance, - title: maintenance.name, + title: maintenance.summary, component: 'maintenance/details', ...options, }); diff --git a/addon/templates/maintenance/equipment/index/details.hbs b/addon/templates/maintenance/equipment/index/details.hbs index e2535d225..a42e49249 100644 --- a/addon/templates/maintenance/equipment/index/details.hbs +++ b/addon/templates/maintenance/equipment/index/details.hbs @@ -1,2 +1,15 @@ - -{{outlet}} \ No newline at end of file + + + {{outlet}} + + diff --git a/addon/templates/maintenance/equipment/index/details/index.hbs b/addon/templates/maintenance/equipment/index/details/index.hbs index e2535d225..b2a2567a4 100644 --- a/addon/templates/maintenance/equipment/index/details/index.hbs +++ b/addon/templates/maintenance/equipment/index/details/index.hbs @@ -1,2 +1 @@ - -{{outlet}} \ No newline at end of file + diff --git a/addon/templates/maintenance/equipment/index/edit.hbs b/addon/templates/maintenance/equipment/index/edit.hbs index e2535d225..73ccb29b7 100644 --- a/addon/templates/maintenance/equipment/index/edit.hbs +++ b/addon/templates/maintenance/equipment/index/edit.hbs @@ -1,2 +1,12 @@ - -{{outlet}} \ No newline at end of file + + + + diff --git a/addon/templates/maintenance/equipment/index/new.hbs b/addon/templates/maintenance/equipment/index/new.hbs index 55a03c4db..db7e10bd3 100644 --- a/addon/templates/maintenance/equipment/index/new.hbs +++ b/addon/templates/maintenance/equipment/index/new.hbs @@ -1,12 +1,11 @@ - + - \ No newline at end of file + diff --git a/addon/templates/maintenance/maintenances.hbs b/addon/templates/maintenance/maintenances.hbs new file mode 100644 index 000000000..c24cd6895 --- /dev/null +++ b/addon/templates/maintenance/maintenances.hbs @@ -0,0 +1 @@ +{{outlet}} diff --git a/addon/templates/maintenance/maintenances/index.hbs b/addon/templates/maintenance/maintenances/index.hbs new file mode 100644 index 000000000..56789f959 --- /dev/null +++ b/addon/templates/maintenance/maintenances/index.hbs @@ -0,0 +1,14 @@ + +{{outlet}} diff --git a/addon/templates/maintenance/maintenances/index/details.hbs b/addon/templates/maintenance/maintenances/index/details.hbs new file mode 100644 index 000000000..2b7547006 --- /dev/null +++ b/addon/templates/maintenance/maintenances/index/details.hbs @@ -0,0 +1,15 @@ + + + {{outlet}} + + diff --git a/addon/templates/maintenance/maintenances/index/details/index.hbs b/addon/templates/maintenance/maintenances/index/details/index.hbs new file mode 100644 index 000000000..40c77ca0f --- /dev/null +++ b/addon/templates/maintenance/maintenances/index/details/index.hbs @@ -0,0 +1 @@ + diff --git a/addon/templates/maintenance/maintenances/index/edit.hbs b/addon/templates/maintenance/maintenances/index/edit.hbs new file mode 100644 index 000000000..51cf8f29f --- /dev/null +++ b/addon/templates/maintenance/maintenances/index/edit.hbs @@ -0,0 +1,12 @@ + + + + diff --git a/addon/templates/maintenance/maintenances/index/new.hbs b/addon/templates/maintenance/maintenances/index/new.hbs new file mode 100644 index 000000000..d88d88c10 --- /dev/null +++ b/addon/templates/maintenance/maintenances/index/new.hbs @@ -0,0 +1,11 @@ + + + + diff --git a/addon/templates/maintenance/parts/index/details.hbs b/addon/templates/maintenance/parts/index/details.hbs index e2535d225..ae72057e3 100644 --- a/addon/templates/maintenance/parts/index/details.hbs +++ b/addon/templates/maintenance/parts/index/details.hbs @@ -1,2 +1,15 @@ - -{{outlet}} \ No newline at end of file + + + {{outlet}} + + diff --git a/addon/templates/maintenance/parts/index/details/index.hbs b/addon/templates/maintenance/parts/index/details/index.hbs index e2535d225..649d89afe 100644 --- a/addon/templates/maintenance/parts/index/details/index.hbs +++ b/addon/templates/maintenance/parts/index/details/index.hbs @@ -1,2 +1 @@ - -{{outlet}} \ No newline at end of file + diff --git a/addon/templates/maintenance/parts/index/edit.hbs b/addon/templates/maintenance/parts/index/edit.hbs index e2535d225..ae6c0a6e5 100644 --- a/addon/templates/maintenance/parts/index/edit.hbs +++ b/addon/templates/maintenance/parts/index/edit.hbs @@ -1,2 +1,12 @@ - -{{outlet}} \ No newline at end of file + + + + diff --git a/addon/templates/maintenance/parts/index/new.hbs b/addon/templates/maintenance/parts/index/new.hbs index 2612bc71e..5ce5a51ac 100644 --- a/addon/templates/maintenance/parts/index/new.hbs +++ b/addon/templates/maintenance/parts/index/new.hbs @@ -1,12 +1,11 @@ - + - \ No newline at end of file + diff --git a/addon/templates/maintenance/work-orders/index/details.hbs b/addon/templates/maintenance/work-orders/index/details.hbs index 07b08d11e..b60346988 100644 --- a/addon/templates/maintenance/work-orders/index/details.hbs +++ b/addon/templates/maintenance/work-orders/index/details.hbs @@ -1,7 +1,8 @@ {{outlet}} - \ No newline at end of file + diff --git a/addon/templates/maintenance/work-orders/index/details/index.hbs b/addon/templates/maintenance/work-orders/index/details/index.hbs index 35ded4f18..b3591413d 100644 --- a/addon/templates/maintenance/work-orders/index/details/index.hbs +++ b/addon/templates/maintenance/work-orders/index/details/index.hbs @@ -1 +1 @@ - \ No newline at end of file + diff --git a/addon/templates/maintenance/work-orders/index/edit.hbs b/addon/templates/maintenance/work-orders/index/edit.hbs index 76640e143..08c7ceebd 100644 --- a/addon/templates/maintenance/work-orders/index/edit.hbs +++ b/addon/templates/maintenance/work-orders/index/edit.hbs @@ -1,14 +1,12 @@ - \ No newline at end of file + diff --git a/addon/templates/maintenance/work-orders/index/new.hbs b/addon/templates/maintenance/work-orders/index/new.hbs index d208c662c..8fdc44c03 100644 --- a/addon/templates/maintenance/work-orders/index/new.hbs +++ b/addon/templates/maintenance/work-orders/index/new.hbs @@ -1,12 +1,11 @@ - + - \ No newline at end of file + diff --git a/server/src/Auth/Schemas/FleetOps.php b/server/src/Auth/Schemas/FleetOps.php index eb2b3660b..6561abd4f 100644 --- a/server/src/Auth/Schemas/FleetOps.php +++ b/server/src/Auth/Schemas/FleetOps.php @@ -113,6 +113,22 @@ class FleetOps 'name' => 'issue', 'actions' => ['export', 'import'], ], + [ + 'name' => 'maintenance', + 'actions' => ['export', 'import'], + ], + [ + 'name' => 'work-order', + 'actions' => ['export', 'import'], + ], + [ + 'name' => 'equipment', + 'actions' => ['export', 'import'], + ], + [ + 'name' => 'part', + 'actions' => ['export', 'import'], + ], [ 'name' => 'custom-field', 'actions' => [], @@ -264,6 +280,23 @@ class FleetOps 'list order', ], ], + [ + 'name' => 'MaintenanceManager', + 'description' => 'Policy for managing maintenance records, work orders, equipment, and parts.', + 'permissions' => [ + 'see extension', + '* maintenance', + '* work-order', + '* equipment', + '* part', + 'see vehicle', + 'list vehicle', + 'view vehicle', + 'see driver', + 'list driver', + 'view driver', + ], + ], [ 'name' => 'OperationsAdmin', 'description' => 'Policy for monitoring activities, issues, and fuel reports within the fleet operations.', @@ -276,6 +309,10 @@ class FleetOps '* fuel-report', '* issue', '* place', + '* maintenance', + '* work-order', + '* equipment', + '* part', ], ], [ @@ -370,6 +407,13 @@ class FleetOps 'OperationsAdmin', ], ], + [ + 'name' => 'Maintenance Technician', + 'description' => 'Role for technicians responsible for managing maintenance records, work orders, equipment, and parts inventory.', + 'policies' => [ + 'MaintenanceManager', + ], + ], [ 'name' => 'Driver Coordinator', 'description' => 'Role responsible for coordinating drivers, vehicles, and orders.', diff --git a/server/src/Console/Commands/ProcessMaintenanceTriggers.php b/server/src/Console/Commands/ProcessMaintenanceTriggers.php new file mode 100644 index 000000000..c0bbd35dd --- /dev/null +++ b/server/src/Console/Commands/ProcessMaintenanceTriggers.php @@ -0,0 +1,125 @@ +option('sandbox'); + $dryRun = (bool) $this->option('dry-run'); + + $this->info('Processing maintenance triggers' . ($dryRun ? ' [DRY RUN]' : '') . ' at ' . Carbon::now()->toDateTimeString()); + + $triggered = 0; + + // ---------------------------------------------------------------- + // 1. Time-based triggers: scheduled maintenances whose scheduled_at + // date has arrived but are still in "scheduled" status. + // ---------------------------------------------------------------- + $due = Maintenance::on($sandbox ? 'sandbox' : 'mysql') + ->withoutGlobalScopes() + ->where('status', 'scheduled') + ->where('scheduled_at', '<=', Carbon::now()) + ->whereNull('deleted_at') + ->get(); + + foreach ($due as $maintenance) { + $this->line("Time-based trigger: maintenance {$maintenance->public_id} is due (scheduled_at: {$maintenance->scheduled_at})"); + + if (!$dryRun) { + $maintenance->status = 'in_progress'; + $maintenance->save(); + + event('maintenance.triggered', $maintenance); + } + + $triggered++; + } + + // ---------------------------------------------------------------- + // 2. Odometer / engine-hour threshold triggers: compare the current + // vehicle readings against the next_service_odometer and + // next_service_engine_hours fields on scheduled maintenances. + // ---------------------------------------------------------------- + $odometricMaintenances = Maintenance::on($sandbox ? 'sandbox' : 'mysql') + ->withoutGlobalScopes() + ->where('status', 'scheduled') + ->where(function ($query) { + $query->whereNotNull('next_service_odometer') + ->orWhereNotNull('next_service_engine_hours'); + }) + ->whereNotNull('vehicle_uuid') + ->whereNull('deleted_at') + ->with('vehicle') + ->get(); + + foreach ($odometricMaintenances as $maintenance) { + $vehicle = $maintenance->vehicle; + + if (!$vehicle) { + continue; + } + + $odometerDue = $maintenance->next_service_odometer && $vehicle->odometer >= $maintenance->next_service_odometer; + $engineHoursDue = $maintenance->next_service_engine_hours && $vehicle->engine_hours >= $maintenance->next_service_engine_hours; + + if ($odometerDue || $engineHoursDue) { + $reason = $odometerDue + ? "odometer {$vehicle->odometer} >= threshold {$maintenance->next_service_odometer}" + : "engine hours {$vehicle->engine_hours} >= threshold {$maintenance->next_service_engine_hours}"; + + $this->line("Threshold trigger: maintenance {$maintenance->public_id} for vehicle {$vehicle->public_id} ({$reason})"); + + if (!$dryRun) { + $maintenance->status = 'in_progress'; + $maintenance->save(); + + event('maintenance.triggered', $maintenance); + } + + $triggered++; + } + } + + $this->info("Processed {$triggered} maintenance trigger(s)" . ($dryRun ? ' (dry run — no records updated)' : '.')); + } +} diff --git a/server/src/Providers/FleetOpsServiceProvider.php b/server/src/Providers/FleetOpsServiceProvider.php index 452ab51ef..41bf0d3c7 100644 --- a/server/src/Providers/FleetOpsServiceProvider.php +++ b/server/src/Providers/FleetOpsServiceProvider.php @@ -61,6 +61,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider \Fleetbase\FleetOps\Console\Commands\SendDriverNotification::class, \Fleetbase\FleetOps\Console\Commands\ReplayVehicleLocations::class, \Fleetbase\FleetOps\Console\Commands\TestEmail::class, + \Fleetbase\FleetOps\Console\Commands\ProcessMaintenanceTriggers::class, ]; /** @@ -98,6 +99,7 @@ public function boot() $schedule->command('fleetops:dispatch-adhoc')->everyMinute()->withoutOverlapping()->storeOutputInDb(); $schedule->command('fleetops:update-estimations')->everyTenMinutes()->withoutOverlapping(); $schedule->command('fleetops:purge-service-quotes')->daily()->withoutOverlapping(); + $schedule->command('fleetops:process-maintenance-triggers')->daily()->withoutOverlapping()->storeOutputInDb(); }); $this->registerNotifications(); $this->registerExpansionsFrom(__DIR__ . '/../Expansions'); From 1b9b20ef160af23ef9282b236b871104519440ad Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 31 Mar 2026 11:39:33 +0800 Subject: [PATCH 02/51] fix missing translations --- .../controllers/connectivity/devices/index/details.js | 2 +- server/src/routes.php | 2 +- translations/en-us.yaml | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/addon/controllers/connectivity/devices/index/details.js b/addon/controllers/connectivity/devices/index/details.js index 84897d86c..ef461a1fc 100644 --- a/addon/controllers/connectivity/devices/index/details.js +++ b/addon/controllers/connectivity/devices/index/details.js @@ -7,7 +7,7 @@ export default class ConnectivityDevicesIndexDetailsController extends Controlle @service hostRouter; get tabs() { - const registeredTabs = this.menuService.getMenuItems('fleet-ops:component:place:details'); + const registeredTabs = this.menuService.getMenuItems('fleet-ops:component:device:details'); return [ { route: 'connectivity.devices.index.details.index', diff --git a/server/src/routes.php b/server/src/routes.php index d6c9b086c..5f0135113 100644 --- a/server/src/routes.php +++ b/server/src/routes.php @@ -424,7 +424,7 @@ function ($router, $controller) { $router->post('{key}/test-credentials', $controller('testCredentials')); }); $router->fleetbaseRoutes('work-orders'); - $router->fleetbaseRoutes('maintenance'); + $router->fleetbaseRoutes('maintenances'); $router->fleetbaseRoutes('equipment'); $router->fleetbaseRoutes('parts'); $router->fleetbaseRoutes('warranties'); diff --git a/translations/en-us.yaml b/translations/en-us.yaml index 8affb1249..e3cdf4c84 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -78,6 +78,7 @@ menu: custom-fields: Custom Fields order-board: Order Board avatars: Avatars + maintenances: Maintenances resource: asset: Asset @@ -245,6 +246,16 @@ column: current-job: Current Job last-seen: Last Seen place: Place + subject: Subject + summary: Summary + total-cost: Total Cost + due-at: Due At + serial-number: Serial Number + manufacturer: Manufacturer + part-number: Part Number + quantity-on-hand: Quantity on Hand + quantity: Quantity + unit-cost: Unit Cost map: visibility-controls: From 89c4cc95e3b7f1eda81db07468e10503da8926a7 Mon Sep 17 00:00:00 2001 From: Fleetbase Dev Date: Mon, 30 Mar 2026 23:47:11 -0400 Subject: [PATCH 03/51] feat(maintenance): restructure all four resource forms into organised 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. --- addon/components/equipment/form.hbs | 191 +++++++++++++++------ addon/components/equipment/form.js | 92 +++++++++-- addon/components/maintenance/form.hbs | 203 ++++++++++++++++++----- addon/components/maintenance/form.js | 90 +++++++++- addon/components/part/form.hbs | 228 +++++++++++++++++++------- addon/components/part/form.js | 78 +++++++-- addon/components/work-order/form.hbs | 177 +++++++++++++++----- addon/components/work-order/form.js | 74 ++++++++- 8 files changed, 898 insertions(+), 235 deletions(-) diff --git a/addon/components/equipment/form.hbs b/addon/components/equipment/form.hbs index 848161cf5..78f7615ae 100644 --- a/addon/components/equipment/form.hbs +++ b/addon/components/equipment/form.hbs @@ -1,36 +1,80 @@ -
- -
-
- {{@resource.name}} - - - -
-
- -
- {{t "common.upload-image-supported"}} -
+
+ + {{! ===== PHOTO ===== }} + +
+
+ {{#if @resource.photo_url}} + Equipment Photo + {{else}} +
+ +
+ {{/if}} + + Upload Photo + + Supports JPG, PNG, GIF
+
+ + {{! ===== IDENTIFICATION ===== }} +
- - +
+ Equipment Details +
+ + - - + + + + + + + + + + + {{! Type & Status }} +
+ Type & Status +
-
+
+
- - - - - - - - - - - - - - + {{! ===== ASSIGNMENT ===== }} + +
+
+ Equipped To +
+ +
+ + {{smart-humanize type}} + +
+ {{#if this.equipableModelName}} + + + {{or model.name model.public_id}} + + + {{/if}} +
+
+ {{! ===== PURCHASE & WARRANTY ===== }} + +
+
+ Purchase Information +
+ + + - +
+ Warranty +
+ - - - -
+ {{! ===== REGISTRY & METADATA ===== }} + + + - + + +
diff --git a/addon/components/equipment/form.js b/addon/components/equipment/form.js index 8d010e504..ebf3b2e38 100644 --- a/addon/components/equipment/form.js +++ b/addon/components/equipment/form.js @@ -1,27 +1,95 @@ import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; import { task } from 'ember-concurrency'; +/** + * Maps a user-facing polymorphic type string to the Ember Data model name + * used by ModelSelect. + */ +const TYPE_TO_MODEL = { + 'fleet-ops:vehicle': 'vehicle', + 'fleet-ops:driver': 'driver', +}; + export default class EquipmentFormComponent extends Component { - /** - * Equipment type options - */ - equipmentTypeOptions = ['ppe', 'refrigeration_unit', 'tool', 'liftgate', 'ramp', 'container', 'pallet_jack', 'forklift', 'safety_equipment', 'communication_device', 'other']; + @service fetch; + @service currentUser; + @service notifications; + + /** Equipment type options. */ + equipmentTypeOptions = [ + 'ppe', + 'refrigeration_unit', + 'tool', + 'liftgate', + 'ramp', + 'container', + 'pallet_jack', + 'forklift', + 'safety_equipment', + 'communication_device', + 'other', + ]; + + /** Status options for equipment. */ + statusOptions = ['available', 'in_use', 'maintenance', 'retired', 'lost', 'damaged']; + + /** Polymorphic equipable type options — the asset this equipment is attached to. */ + equipableTypeOptions = ['fleet-ops:vehicle', 'fleet-ops:driver']; + + /** Derived Ember Data model name for the currently selected equipable type. */ + @tracked equipableModelName = null; + + constructor(owner, args) { + super(owner, args); + const { resource } = args; + if (resource?.equipable_type) { + this.equipableModelName = TYPE_TO_MODEL[resource.equipable_type] ?? null; + } + } /** - * Status options for equipment + * Handles a change to the equipable type selector. Resets the equipable + * relationship so a stale association is not persisted. */ - statusOptions = ['available', 'in_use', 'maintenance', 'retired', 'lost', 'damaged']; + @action onEquipableTypeChange(type) { + this.args.resource.equipable_type = type; + this.args.resource.equipable_uuid = null; + this.args.resource.equipable = null; + this.equipableModelName = TYPE_TO_MODEL[type] ?? null; + } + + /** Assigns the selected equipable model to the resource. */ + @action assignEquipable(model) { + this.args.resource.equipable = model; + this.args.resource.equipable_uuid = model?.id ?? null; + } /** - * Task to handle photo upload + * Handles photo upload using the Fleetbase fetch service upload pattern. */ @task *handlePhotoUpload(file) { try { - const response = yield file.upload(this.args.resource); - this.args.resource.photo_uuid = response.uuid; - this.args.resource.photo_url = response.url; - } catch (error) { - console.error('Photo upload failed:', error); + yield this.fetch.uploadFile.perform( + file, + { + path: `uploads/${this.currentUser.companyId}/equipment/${this.args.resource.id}`, + subject_uuid: this.args.resource.id, + subject_type: 'fleet-ops:equipment', + type: 'equipment_photo', + }, + (uploadedFile) => { + this.args.resource.setProperties({ + photo_uuid: uploadedFile.id, + photo_url: uploadedFile.url, + photo: uploadedFile, + }); + } + ); + } catch (err) { + this.notifications.error('Unable to upload photo: ' + err.message); } } } diff --git a/addon/components/maintenance/form.hbs b/addon/components/maintenance/form.hbs index e61554bb9..2d324d10c 100644 --- a/addon/components/maintenance/form.hbs +++ b/addon/components/maintenance/form.hbs @@ -1,10 +1,24 @@ -
- +
+ + {{! ===== IDENTIFICATION ===== }} +
+
+ Maintenance Details +
- + +
+ Type, Status & Priority +
-
-
+
+
+ + {{! ===== ASSET & WORK ORDER ===== }} + +
+
+ Maintainable Asset +
+ +
+ + {{smart-humanize type}} + +
+
+ {{#if this.maintainableModelName}} + + + {{or model.name model.public_id}} + + + {{/if}} + +
+ Performed By +
+ +
+ + {{smart-humanize type}} + +
+
+ {{#if this.performedByModelName}} + + + {{or model.name model.public_id}} + + + {{/if}} - +
+ Linked Work Order +
+ - {{model.code}} - {{model.subject}} + {{or model.subject model.code model.public_id}} +
+
+ {{! ===== SCHEDULING & READINGS ===== }} + +
+
+ Dates +
- - - - +
+ Asset Readings at Service +
+ + - - - + + +
+
- + {{! ===== COSTS ===== }} + +
+
+ Cost Breakdown +
+ - - - +
+
- + {{! ===== NOTES ===== }} + +
+