diff --git a/.github/workflows/postman.yml b/.github/workflows/postman.yml index 24e38fe88..d58143299 100644 --- a/.github/workflows/postman.yml +++ b/.github/workflows/postman.yml @@ -5,14 +5,9 @@ name: API Contract (Postman) # fleetbase/fleetbase. Requires org secrets POSTMAN_API_KEY + _GITHUB_AUTH_TOKEN # (inherited); no-ops until POSTMAN_API_KEY is set. # -# Deliberately unpinned. The reusable workflow defaults to booting fleetbase/fleetbase@main -# against fleetbase/fleetbase-api:latest, so every release is picked up automatically and -# there is no ref here to remember to bump. Each run records the image digest it actually -# resolved in its job summary, so a result stays traceable. To reproduce an older run: -# -# with: -# fleetbase-ref: v0.7.53 -# api-image: fleetbase/fleetbase-api:v0.7.53 +# Pin the workflow and host checkout together so branch installation resolves the +# required published Core release before migrating and exercising this module. +# The workflow records the API image digest and resolved dependencies for traceability. on: push: @@ -25,8 +20,9 @@ permissions: jobs: contract: - uses: fleetbase/fleetbase/.github/workflows/api-contract.yml@main + uses: fleetbase/fleetbase/.github/workflows/api-contract.yml@880c7392c67c93e3a65f01916d7a04036c098935 with: + fleetbase-ref: 880c7392c67c93e3a65f01916d7a04036c098935 collections: "Fleetbase API" build-from-source: false # Without this the run tests the version of fleetbase/fleetops-api baked into the diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 84eb44839..e9699443b 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -53,6 +53,7 @@ jobs: run: php scripts/coverage-summary.php coverage/clover.xml --fail-under=100 - name: Upload Coverage Baseline + if: ${{ always() && hashFiles('coverage/clover.xml') != '' }} uses: actions/upload-artifact@v4 with: name: fleetops-coverage-clover diff --git a/RELEASE.md b/RELEASE.md index 32ebd138d..0c8854947 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,23 +1,56 @@ -> v0.6.69 ~ "Fleet-Ops tools for Fleetbase AI" +> v0.6.70 ~ "Telematics data retention" --- ## What's New -- **Fleet-Ops works with Fleetbase AI tool calling.** Creating orders from the AI prompt works again with the tool-calling assistant, and the assistant can also propose an optimized waypoint sequence for an order and explain what an order or resource import needs. Every change is a preview card the user confirms. -- **Fleet-Ops console actions.** The assistant can offer to open Fleet-Ops pages and dialogs, such as **Operations › Orders** and **New Order**, as confirmation cards. -- **Resource search for the assistant.** A `fleetops_search` tool finds orders, vehicles, drivers, work orders, maintenances, devices, sensors and telematics records by id, name, plate, VIN, email or phone. +- **Telematics Data settings.** A new Fleet-Ops → Settings → Telematics Data page lets each organization decide how long telemetry is kept: device events, raw event payloads, positions, processed and quarantined delivery envelopes, and sync run diagnostics. Administrators set system-wide defaults from the admin console (Fleet-Ops Config → Telematics Data). A value of 0 keeps rows forever. +- **Scheduled retention sweep.** `fleetops:prune-telematics-data` runs every fifteen minutes and applies each organization's policy in bounded batches, so a large backlog is drained gradually without stalling ingestion. Defaults: events 30 days, raw payloads stripped after 7 days, positions 90 days, processed deliveries 24 hours, quarantined deliveries 7 days, sync runs 7 days. +- **Storage usage and on-demand cleanup.** The settings page shows rows, oldest row and estimated size per table for your organization, and "Run cleanup now" queues an immediate retention run. --- ## Fixes -- AI resource search no longer fails on every call with `Unknown column 'sensor_type'`, and database errors are no longer passed to the model. +- Device events no longer store the raw provider unit twice. `payload` keeps the raw unit; `meta` holds only the normalized block (speed, heading, odometer, ignition, fuel level, timing). New event rows are roughly half the size. +- Telemetry-driven saves (device, event, position, vehicle, sensors) no longer write to the activity log on every poll. Organizations that want them can enable "Log telemetry activity" in Telematics Data settings. Manual edits are logged as before. +- Delivery inbox and sync run retention moved from the drain command into the retention sweep; `fleetops:drain-telematic-inbox` now only recovers deliveries and finishes interrupted runs. + +--- +## Upgrade notes +- New indexes on `device_events (company_uuid, created_at)`, `positions (company_uuid, created_at)` and `telematic_sync_runs (telematic_uuid, updated_at)`; on very large tables run `php artisan migrate` in a maintenance window. +- Retention is enabled by default. Existing rows older than the defaults are removed over successive runs after upgrading; set the values to 0 before upgrading if you need to keep history indefinitely. +- Deleting rows frees space inside the database files, not on disk. Run `OPTIMIZE TABLE` off-peak to return space to the operating system. +- Run `php artisan fleetbase:create-permissions` to register the `telematics-settings` permission and the Telematics Settings Manager policy and role. + +--- +## Testing +- Backend tests cover the retention policy layering and clamping, the prune command (per-company policies, compaction, inbox tables via connections, orphans, batch caps, dry runs, filters, locking), the settings and storage usage endpoints, the index migrations, the on-demand job, and the meta and activity-log changes to ingestion. +- Ember tests cover the settings route guard, the settings controller, and the admin defaults component. +> v0.6.70 ~ "Order reporting and a cleaner default dashboard" + +--- +## What's New +- **Report on orders, their items and tracking.** The report builder can answer questions like "Which products sold the most this month?" and "What did orders total this month?". + - Orders expose their ID and Internal ID, and relationships for tracking (tracking number, region, latest status), Order Config, Customer Vendor, Created By, Purchase Rate › Service Quote and Payload Return. + - `Payload` exposes its items with name, SKU, price, dimensions, metadata and destination. Select an item column for one row per item, or group by one to summarise per product. + - Summary columns count orders distinctly, so they stay correct when item rows are selected. Distance, duration and transaction amount have totals and averages. + - `meta` has no fixed shape, so no column assumes a key in it. Read keys with a computed column, for example `CAST(JSON_UNQUOTE(JSON_EXTRACT(payload.entities.meta, '$.quantity')) AS DECIMAL(15,2))`. + - Column labels drop the table name ("Type", not "Order Type"). Distance and duration are labelled in meters and seconds, and money columns are marked "(minor units)". +- **A cleaner default dashboard.** Fleet-Ops widgets declare their place on the default dashboard: Radar first, then Active Orders and Drivers Online, a full-width Live Fleet map, and Revenue Trend, Top Drivers and Maintenance side by side. +- **Live Fleet widget cards match the live map.** Clicking a driver or vehicle on the widget map shows the same card as **Operations › Live Map**. + +--- +## Fixes +- Custom field values on fuel reports and service areas are saved. They were dropped because the save hooks read the snake_case payload root, while the console sends `fuelReport` and `serviceArea`. +- The report schema no longer references 13 columns and join keys that don't exist: driver name, email and phone and the driver's vehicle, the vehicle's driver, and the fuel report cost, odometer and date. Transaction summaries sum `transaction.amount`. +- Every report table, and the item join, leaves soft-deleted rows out on core-api v1.6.64 and later. --- ## Dependencies -- `@fleetbase/fleetops-data` upgraded to `^0.2.2`, which adds the read-only login state (`is_staff_linked`, `login_status`) to the driver and contact models. -- `@fleetbase/ember-ui` upgraded to `^0.4.3`, which provides the `btn-auth` style the Track Order button on the login page uses. +- Order items, JSON totals and soft deletes in reports need `fleetbase/core-api` v1.6.64. The schema still registers on older versions. +- The report builder and the default dashboard order ship in `@fleetbase/ember-ui` v0.4.4. --- ## Testing -- Unit tests cover the new AI tools and console commands, and the AI capability registration. +- Every declared report column and join key was checked against the database schema, and order reports were run against real storefront orders on MySQL in strict mode. +- Tests cover the report schema at 100% line coverage, the Live Fleet map cards and the default dashboard order. --- ## Need help? diff --git a/addon/components/admin/telematics-settings.hbs b/addon/components/admin/telematics-settings.hbs new file mode 100644 index 000000000..e415ce894 --- /dev/null +++ b/addon/components/admin/telematics-settings.hbs @@ -0,0 +1,143 @@ +
+ + Configure telematics retention policy and activity logging for all organizations. Organizations can choose device event and position history retention within these limits from Fleet-Ops › Settings › Telematics. + A maximum takes precedence over organization preferences, inherited defaults and requests to keep history forever. Reducing a maximum can delete existing history at the next cleanup. + + + +
+ + + + + + +
+
+ + +
+ + + + + + +
+
+ + + + + + + + +
+ + + + + + + + + +
+
+ + + + + +
+
+ + +

+ Storage usage includes telematics data across all organizations. Cleanup applies system-wide, using the saved system policy and each organization's history preferences within that policy. +

+
+
+ {{#if this.usageRows.length}} +
+ + + + + + + + + + + {{#each this.usageRows as |row|}} + + + + + + + {{/each}} + +
{{t "settings.telematics.table"}}{{t "settings.telematics.rows"}}{{t "settings.telematics.estimated-size"}}{{t "settings.telematics.oldest"}}
+
{{row.label}}
+ {{#if row.raw_payload_rows}} +
+ {{t "settings.telematics.raw-payload-rows" count=(format-number row.raw_payload_rows) compactable=(format-number row.compactable_rows)}} +
+ {{/if}} +
+ {{#if (eq row.rows null)}} + {{t "settings.telematics.not-available"}} + {{else if row.rows_estimated}} + {{t "settings.telematics.estimated-rows" count=(format-number row.rows)}} + {{else}} + {{format-number row.rows}} + {{/if}} + {{#if (or row.estimated_bytes (eq row.estimated_bytes 0))}}{{format-bytes row.estimated_bytes}}{{else}}{{t "settings.telematics.not-available"}}{{/if}}{{#if row.oldest}}{{row.oldest}}{{else}}{{t "settings.telematics.not-available"}}{{/if}}
+
+

{{t "settings.telematics.space-reclaim-note"}}

+ {{else if this.loadUsage.isRunning}} +
+ +
+ {{else}} +
+ {{t "settings.telematics.usage-unavailable"}} +
+ {{/if}} +
+ +
diff --git a/addon/components/admin/telematics-settings.js b/addon/components/admin/telematics-settings.js new file mode 100644 index 000000000..f2d19d9ba --- /dev/null +++ b/addon/components/admin/telematics-settings.js @@ -0,0 +1,145 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task, timeout } from 'ember-concurrency'; + +/** + * System-wide telematics policy, storage diagnostics and operational controls. + */ +export default class AdminTelematicsSettingsComponent extends Component { + @service fetch; + @service notifications; + @service intl; + @service modalsManager; + @tracked eventRetentionDays = 30; + @tracked maxEventRetentionDays = 0; + @tracked eventCompactAfterDays = 7; + @tracked positionRetentionDays = 90; + @tracked maxPositionRetentionDays = 0; + @tracked processedRetentionHours = 24; + @tracked quarantineRetentionDays = 7; + @tracked syncRunRetentionDays = 7; + @tracked logTelemetryActivity = false; + @tracked settingsLoaded = false; + @tracked usage = null; + + constructor() { + super(...arguments); + this.loadSettings.perform(); + this.loadUsage.perform(); + } + + @task({ drop: true }) *loadSettings() { + try { + const settings = yield this.fetch.get('fleet-ops/settings/admin-telematics-settings'); + this.applySettings(settings); + this.settingsLoaded = true; + } catch (error) { + this.notifications.serverError(error); + } + } + + @task({ drop: true }) *saveSettings() { + if (!this.settingsLoaded) { + return; + } + + try { + const settings = yield this.fetch.post('fleet-ops/settings/admin-telematics-settings', this.settingsPayload); + this.applySettings(settings); + this.notifications.success('System telematics settings saved.'); + } catch (error) { + this.notifications.serverError(error); + } + } + + /** Load system-wide storage usage across all organizations. */ + @task({ drop: true }) *loadUsage() { + const abortController = new AbortController(); + const requestTimeout = setTimeout(() => abortController.abort(), 30000); + + try { + // Pass the signal as request data; fetch.get options do not forward it. + const usage = yield this.fetch.request('fleet-ops/settings/telematics-storage-usage?include_payload_counts=0', 'GET', { signal: abortController.signal }); + if (usage?.scope !== 'system') { + throw new Error(this.intl.t('settings.telematics.usage-unavailable')); + } + this.usage = usage; + } catch (error) { + this.notifications.serverError(abortController.signal.aborted ? new Error(this.intl.t('settings.telematics.usage-unavailable')) : error); + } finally { + clearTimeout(requestTimeout); + abortController.abort(); + } + } + + /** Queue cleanup for all organizations using the saved policy. */ + @task({ drop: true }) *runCleanup() { + try { + const cleanup = yield this.fetch.post('fleet-ops/settings/telematics-retention/run'); + if (cleanup?.scope !== 'system') { + throw new Error(this.intl.t('settings.telematics.cleanup-unavailable')); + } + this.notifications.success(this.intl.t('settings.telematics.cleanup-queued')); + yield timeout(5000); + yield this.loadUsage.perform(); + } catch (error) { + this.notifications.serverError(error); + } + } + + @action confirmRunCleanup() { + return this.modalsManager.confirm({ + title: this.intl.t('settings.telematics.run-cleanup'), + body: this.intl.t('settings.telematics.run-cleanup-confirm'), + acceptButtonText: this.intl.t('settings.telematics.run-cleanup'), + acceptButtonIcon: 'broom', + onConfirm: () => this.runCleanup.perform(), + }); + } + + get usageRows() { + const tables = this.usage?.tables ?? {}; + + return ['device_events', 'positions', 'telematic_deliveries', 'telematic_sync_runs'] + .filter((table) => tables[table]) + .map((table) => ({ table, label: this.intl.t(`settings.telematics.tables.${table}`), ...tables[table] })); + } + + get settingsPayload() { + return { + event_retention_days: this.toInteger(this.eventRetentionDays), + max_event_retention_days: this.maxEventRetentionDays, + event_compact_after_days: this.toInteger(this.eventCompactAfterDays), + position_retention_days: this.toInteger(this.positionRetentionDays), + max_position_retention_days: this.maxPositionRetentionDays, + processed_retention_hours: this.toInteger(this.processedRetentionHours), + quarantine_retention_days: this.toInteger(this.quarantineRetentionDays), + sync_run_retention_days: this.toInteger(this.syncRunRetentionDays), + log_telemetry_activity: Boolean(this.logTelemetryActivity), + }; + } + + applySettings(settings = {}) { + if (!Object.prototype.hasOwnProperty.call(settings ?? {}, 'max_event_retention_days') || !Object.prototype.hasOwnProperty.call(settings ?? {}, 'max_position_retention_days')) { + throw new Error(this.intl.t('settings.telematics.settings-unavailable')); + } + + this.eventRetentionDays = settings.event_retention_days ?? this.eventRetentionDays; + this.maxEventRetentionDays = settings.max_event_retention_days ?? this.maxEventRetentionDays; + this.eventCompactAfterDays = settings.event_compact_after_days ?? this.eventCompactAfterDays; + this.positionRetentionDays = settings.position_retention_days ?? this.positionRetentionDays; + this.maxPositionRetentionDays = settings.max_position_retention_days ?? this.maxPositionRetentionDays; + this.processedRetentionHours = settings.processed_retention_hours ?? this.processedRetentionHours; + this.quarantineRetentionDays = settings.quarantine_retention_days ?? this.quarantineRetentionDays; + this.syncRunRetentionDays = settings.sync_run_retention_days ?? this.syncRunRetentionDays; + this.logTelemetryActivity = settings.log_telemetry_activity ?? this.logTelemetryActivity; + } + + toInteger(value) { + const number = parseInt(value, 10); + + return Number.isFinite(number) && number > 0 ? number : 0; + } +} diff --git a/addon/components/fuel-integration/hub.hbs b/addon/components/fuel-integration/hub.hbs index 183657974..65ad63610 100644 --- a/addon/components/fuel-integration/hub.hbs +++ b/addon/components/fuel-integration/hub.hbs @@ -17,7 +17,7 @@
diff --git a/addon/components/layout/fleet-ops-sidebar.js b/addon/components/layout/fleet-ops-sidebar.js index 0abf9056f..8a2e67bad 100644 --- a/addon/components/layout/fleet-ops-sidebar.js +++ b/addon/components/layout/fleet-ops-sidebar.js @@ -151,14 +151,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component { this.createItem('menu.contacts', 'address-book', 'management.contacts', 'fleet-ops list contact', 'fleet-ops see contact'), this.createItem('menu.places', 'location-dot', 'management.places', 'fleet-ops list place', 'fleet-ops see place'), this.createItem('menu.fuel-reports', 'gas-pump', 'management.fuel-reports', 'fleet-ops list fuel-report', 'fleet-ops see fuel-report'), - this.createItem('menu.fuel-transactions', 'credit-card', 'management.fuel-transactions', 'fleet-ops list fuel-report', 'fleet-ops see fuel-report'), + this.createItem('menu.fuel-transactions', 'credit-card', 'management.fuel-transactions', 'fleet-ops list fuel-provider-transaction', 'fleet-ops see fuel-provider-transaction'), this.createItem('menu.issues', 'triangle-exclamation', 'management.issues', 'fleet-ops list issue', 'fleet-ops see issue'), ]); } get maintenanceItems() { return this.withRegistryItems('maintenance', [ - this.createHubItem('Maintenance Hub', 'wrench', 'maintenance.index', 'fleet-ops list maintenance-schedule', 'fleet-ops see maintenance-schedule', [ + this.createHubItem('Maintenance Hub', 'wrench', 'maintenance.index', 'fleet-ops list work-order', 'fleet-ops see work-order', [ 'maintenance hub', 'service readiness', 'maintenance control panel', @@ -182,7 +182,9 @@ export default class LayoutFleetOpsSidebarComponent extends Component { get connectivityItems() { return this.withRegistryItems('connectivity', [ this.createHubItem(this.intl.t('menu.telematics'), 'satellite-dish', 'connectivity.telematics', 'fleet-ops list telematic', 'fleet-ops see telematic', ['connectivity hub']), - this.createItem('menu.fuel-providers', 'gas-pump', 'connectivity.fuel-providers', 'fleet-ops list fuel-report', 'fleet-ops see fuel-report', ['fuel integrations']), + this.createItem('menu.fuel-providers', 'gas-pump', 'connectivity.fuel-providers', 'fleet-ops list fuel-provider-connection', 'fleet-ops see fuel-provider-connection', [ + 'fuel integrations', + ]), this.createItem('menu.devices', 'hard-drive', 'connectivity.devices', 'fleet-ops list device', 'fleet-ops see device'), this.createItem('menu.sensors', 'temperature-full', 'connectivity.sensors', 'fleet-ops list sensor', 'fleet-ops see sensor'), this.createItem('menu.events', 'stream', 'connectivity.events', 'fleet-ops list device-event', 'fleet-ops see device-event'), @@ -191,18 +193,14 @@ export default class LayoutFleetOpsSidebarComponent extends Component { get analyticsItems() { return this.withRegistryItems('analytics', [ - this.createHubItem('Dashboard', 'chart-line', 'analytics.index', 'iam list report', 'fleet-ops see report', ['dashboard', 'fleetops dashboard', 'metrics']), - this.createItem('menu.reports', 'file-import', 'analytics.reports', 'iam list report', 'fleet-ops see report'), + this.createHubItem('Dashboard', 'chart-line', 'analytics.index', 'fleet-ops view analytics', 'fleet-ops see analytics', ['dashboard', 'fleetops dashboard', 'metrics']), + this.createItem('menu.reports', 'file-import', 'analytics.reports', 'iam list report', 'iam see report'), ]); } get settingsItems() { return this.withRegistryItems('settings', [ - this.createHubItem('Settings Hub', 'sliders', 'settings.index', 'fleet-ops view navigator-settings', 'fleet-ops see navigator-settings', [ - 'settings hub', - 'configuration dashboard', - 'setup focus', - ]), + this.createHubItem('Settings Hub', 'sliders', 'settings.index', null, null, ['settings hub', 'configuration dashboard', 'setup focus']), this.createItem('menu.navigator-app', 'location-arrow', 'settings.navigator-app', 'fleet-ops view navigator-settings', 'fleet-ops see navigator-settings'), this.createItem('menu.map', 'map', 'settings.map', 'fleet-ops view map-settings', 'fleet-ops see map-settings'), this.createItem('menu.payments', 'cash-register', 'settings.payments', 'fleet-ops view payments', 'fleet-ops see payments'), @@ -210,8 +208,9 @@ export default class LayoutFleetOpsSidebarComponent extends Component { this.createItem('menu.routing', 'route', 'settings.routing', 'fleet-ops view routing-settings', 'fleet-ops see routing-settings'), this.createItem('menu.orchestrator', 'circle-nodes', 'settings.orchestrator', 'fleet-ops view routing-settings', 'fleet-ops see routing-settings'), this.createItem('menu.scheduling', 'calendar-days', 'settings.scheduling', 'fleet-ops view scheduling-settings', 'fleet-ops see scheduling-settings'), - this.createItem('menu.custom-fields', 'pen-to-square', 'settings.custom-fields', 'fleet-ops view custom-field', 'fleet-ops see custom-field'), - this.createItem('menu.avatars', 'icons', 'settings.avatars', 'fleet-ops view avatar', 'fleet-ops see avatar'), + this.createItem('menu.telematics-settings', 'satellite-dish', 'settings.telematics', 'fleet-ops view telematics-settings', 'fleet-ops see telematics-settings'), + this.createItem('menu.custom-fields', 'pen-to-square', 'settings.custom-fields', 'fleet-ops list custom-field', 'fleet-ops see custom-field'), + this.createItem('menu.avatars', 'icons', 'settings.avatars', 'fleet-ops list avatar', 'fleet-ops see avatar'), ]); } @@ -265,18 +264,36 @@ export default class LayoutFleetOpsSidebarComponent extends Component { } createBranch({ id, label, icon, route, defaultRoute, requiresVisibleChildren = false, children, keywords = [] }) { + const visibleChildren = children.filter((item) => item.visible !== false); + return { id, label, icon, route: this.fullRoute(route), - defaultRoute: this.fullRoute(defaultRoute), + defaultRoute: this.resolveDefaultRoute(this.fullRoute(defaultRoute), visibleChildren), requiresVisibleChildren, - children: children.filter((item) => item.visible !== false), + children: visibleChildren, keywords, }; } + /** + * Keep the branch's default route when the user may open it; otherwise fall back to the + * first child route they are permitted to open, so clicking the branch does not land on a + * route guard that bounces them back out. + */ + resolveDefaultRoute(defaultRoute, children = []) { + const permitted = (item) => [item.visiblePermission, item.permission].every((permission) => !permission || this.abilities.can(permission)); + const defaultChild = children.find((item) => item.route === defaultRoute); + + if (!defaultChild || permitted(defaultChild)) { + return defaultRoute; + } + + return children.find((item) => item.route && permitted(item))?.route ?? defaultRoute; + } + createItem(intl, icon, route, permission, ability, keywords = []) { return { priority: this.defaultPriorityForRoute(route), @@ -290,7 +307,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component { }; } - createHubItem(label, icon, route, _permission, _ability, keywords = []) { + createHubItem(label, icon, route, permission, ability, keywords = []) { return { pinnedFirst: true, priority: this.defaultPriorityForRoute(route), @@ -298,6 +315,8 @@ export default class LayoutFleetOpsSidebarComponent extends Component { description: label, icon, route: this.fullRoute(route), + permission, + visiblePermission: ability, isNavigationHub: true, keywords: [label, route, ...keywords].filter(Boolean), }; @@ -387,8 +406,9 @@ export default class LayoutFleetOpsSidebarComponent extends Component { 'settings.routing': 5, 'settings.orchestrator': 6, 'settings.scheduling': 7, - 'settings.custom-fields': 8, - 'settings.avatars': 9, + 'settings.telematics': 8, + 'settings.custom-fields': 9, + 'settings.avatars': 10, }; return priorities[route] ?? 0; diff --git a/addon/components/map/leaflet-live-map.hbs b/addon/components/map/leaflet-live-map.hbs index a82b064b5..920918b44 100644 --- a/addon/components/map/leaflet-live-map.hbs +++ b/addon/components/map/leaflet-live-map.hbs @@ -83,108 +83,10 @@ as |marker| > -
-
- - {{driver.name}} -
- {{n-a driver.meta.status_label (smart-humanize driver.status)}} -
-
-
-
-
ID
-
{{n-a driver.public_id driver.id}}
-
-
-
Phone
-
{{n-a driver.phone}}
-
-
-
Vehicle
-
{{n-a driver.vehicle_name}}
-
-
-
Email
-
{{n-a driver.email}}
-
-
-
Order
-
{{n-a driver.meta.current_order_reference}}
-
-
-
Speed
-
{{n-a driver.meta.speed_label}}
-
-
-
Heading
-
{{n-a driver.meta.heading_label}}
-
-
-
Location
-
{{n-a - driver.meta.location_coordinates - (point-coordinates driver.location) - }}
-
-
-
+
-
-
- - {{driver.name}} -
- {{n-a driver.meta.status_label (smart-humanize driver.status)}} -
-
-
-
-
ID
-
{{n-a driver.public_id driver.id}}
-
-
-
Phone
-
{{n-a driver.phone}}
-
-
-
Vehicle
-
{{n-a driver.vehicle_name}}
-
-
-
Email
-
{{n-a driver.email}}
-
-
-
Order
-
{{n-a driver.meta.current_order_reference}}
-
-
-
Speed
-
{{n-a driver.meta.speed_label}}
-
-
-
Heading
-
{{n-a driver.meta.heading_label}}
-
-
-
Location
-
{{n-a - driver.meta.location_coordinates - (point-coordinates driver.location) - }}
-
-
-
+
{{/each}} @@ -202,150 +104,10 @@ as |marker| > -
-
- - {{vehicle.displayName}} -
- {{n-a vehicle.meta.status_label}} -
-
-
-
-
Vehicle #
-
{{n-a - vehicle.internal_id - vehicle.public_id - vehicle.plate_number - vehicle.serial_number - vehicle.vin - }}
-
-
-
Driver
-
{{n-a vehicle.driver_name}}
-
-
-
{{t "resource.trailers"}}
-
- {{#each vehicle.trailers as |trailer|}} -
- - {{or trailer.displayName trailer.display_name trailer.name trailer.public_id}} -
- {{else}} - - - {{/each}} -
-
-
-
{{t "resource.devices"}}
-
- {{#each vehicle.devices as |device|}} -
- - {{or device.displayName device.display_name device.name device.device_id device.public_id}} -
- {{else}} - - - {{/each}} -
-
-
-
Order
-
{{n-a vehicle.meta.current_order_reference}}
-
-
-
Speed
-
{{n-a vehicle.meta.speed_label}}
-
-
-
Heading
-
{{n-a vehicle.meta.heading_label}}
-
-
-
Location
-
{{n-a vehicle.meta.location_coordinates}}
-
-
-
+
-
-
- - {{vehicle.displayName}} -
- {{n-a vehicle.meta.status_label}} -
-
-
-
-
Vehicle #
-
{{n-a - vehicle.internal_id - vehicle.public_id - vehicle.plate_number - vehicle.serial_number - vehicle.vin - }}
-
-
-
Driver
-
{{n-a vehicle.driver_name}}
-
-
-
{{t "resource.trailers"}}
-
- {{#each vehicle.trailers as |trailer|}} -
- - {{or trailer.displayName trailer.display_name trailer.name trailer.public_id}} -
- {{else}} - - - {{/each}} -
-
-
-
{{t "resource.devices"}}
-
- {{#each vehicle.devices as |device|}} -
- - {{or device.displayName device.display_name device.name device.device_id device.public_id}} -
- {{else}} - - - {{/each}} -
-
-
-
Order
-
{{n-a vehicle.meta.current_order_reference}}
-
-
-
Speed
-
{{n-a vehicle.meta.speed_label}}
-
-
-
Heading
-
{{n-a vehicle.meta.heading_label}}
-
-
-
Location
-
{{n-a vehicle.meta.location_coordinates}}
-
-
-
+
{{/each}} diff --git a/addon/components/map/leaflet-live-map.js b/addon/components/map/leaflet-live-map.js index ae7727f8a..ded9fcd65 100644 --- a/addon/components/map/leaflet-live-map.js +++ b/addon/components/map/leaflet-live-map.js @@ -298,7 +298,8 @@ export default class MapLeafletLiveMapComponent extends Component { } @task *loadResource(path, options = {}) { - if (this.abilities.cannot(`fleet-ops list ${path}`)) return []; + // Permissions use the singular resource name (e.g. `fleet-ops list vehicle`), not the plural endpoint path. + if (this.abilities.cannot(`fleet-ops list ${singularize(path)}`)) return []; if (path === 'service-areas') { const serviceAreas = yield this.serviceAreaActions.loadAll.perform(); diff --git a/addon/components/map/marker-card/driver.hbs b/addon/components/map/marker-card/driver.hbs new file mode 100644 index 000000000..ff15f7e6d --- /dev/null +++ b/addon/components/map/marker-card/driver.hbs @@ -0,0 +1,57 @@ +{{! + The driver card the fleet-ops live map shows in a marker's popup and hover tooltip. + Shared with the dashboard's Live Fleet widget so both look the same. @driver is a driver + model or its index resource payload (the same attributes, plain JSON). + @variant: "popup" (default, a dark panel) or "tooltip" (inherits the tooltip's panel). +}} +
+
+ + {{@driver.name}} +
+ {{n-a @driver.meta.status_label (smart-humanize @driver.status)}} +
+
+
+
+
ID
+
{{n-a @driver.public_id @driver.id}}
+
+
+
Phone
+
{{n-a @driver.phone}}
+
+
+
Vehicle
+
{{n-a @driver.vehicle_name}}
+
+
+
Email
+
{{n-a @driver.email}}
+
+
+
Order
+
{{n-a @driver.meta.current_order_reference}}
+
+
+
Speed
+
{{n-a @driver.meta.speed_label}}
+
+
+
Heading
+
{{n-a @driver.meta.heading_label}}
+
+
+
Location
+
{{n-a + @driver.meta.location_coordinates + (point-coordinates @driver.location) + }}
+
+
+
+ diff --git a/addon/components/map/marker-card/driver.js b/addon/components/map/marker-card/driver.js new file mode 100644 index 000000000..fa8505356 --- /dev/null +++ b/addon/components/map/marker-card/driver.js @@ -0,0 +1,3 @@ +import templateOnly from '@ember/component/template-only'; + +export default templateOnly(); diff --git a/addon/components/map/marker-card/vehicle.hbs b/addon/components/map/marker-card/vehicle.hbs new file mode 100644 index 000000000..a6f5544cc --- /dev/null +++ b/addon/components/map/marker-card/vehicle.hbs @@ -0,0 +1,78 @@ +{{! + The vehicle card the fleet-ops live map shows in a marker's popup and hover tooltip. + Shared with the dashboard's Live Fleet widget so both look the same. @vehicle is a vehicle + model or its index resource payload (the same attributes, plain JSON). + @variant: "popup" (default, a dark panel) or "tooltip" (inherits the tooltip's panel). +}} +
+
+ + {{or @vehicle.displayName @vehicle.display_name @vehicle.name}} +
+ {{n-a @vehicle.meta.status_label}} +
+
+
+
+
Vehicle #
+
{{n-a + @vehicle.internal_id + @vehicle.public_id + @vehicle.plate_number + @vehicle.serial_number + @vehicle.vin + }}
+
+
+
Driver
+
{{n-a @vehicle.driver_name}}
+
+
+
{{t "resource.trailers"}}
+
+ {{#each @vehicle.trailers as |trailer|}} +
+ + {{or trailer.displayName trailer.display_name trailer.name trailer.public_id}} +
+ {{else}} + - + {{/each}} +
+
+
+
{{t "resource.devices"}}
+
+ {{#each @vehicle.devices as |device|}} +
+ + {{or device.displayName device.display_name device.name device.device_id device.public_id}} +
+ {{else}} + - + {{/each}} +
+
+
+
Order
+
{{n-a @vehicle.meta.current_order_reference}}
+
+
+
Speed
+
{{n-a @vehicle.meta.speed_label}}
+
+
+
Heading
+
{{n-a @vehicle.meta.heading_label}}
+
+
+
Location
+
{{n-a @vehicle.meta.location_coordinates}}
+
+
+
+ diff --git a/addon/components/map/marker-card/vehicle.js b/addon/components/map/marker-card/vehicle.js new file mode 100644 index 000000000..fa8505356 --- /dev/null +++ b/addon/components/map/marker-card/vehicle.js @@ -0,0 +1,3 @@ +import templateOnly from '@ember/component/template-only'; + +export default templateOnly(); diff --git a/addon/components/telematic/hub.hbs b/addon/components/telematic/hub.hbs index ad094657c..a567a6c97 100644 --- a/addon/components/telematic/hub.hbs +++ b/addon/components/telematic/hub.hbs @@ -15,7 +15,7 @@
diff --git a/addon/components/widget/live-fleet.hbs b/addon/components/widget/live-fleet.hbs index 82965680b..01ef5974d 100644 --- a/addon/components/widget/live-fleet.hbs +++ b/addon/components/widget/live-fleet.hbs @@ -28,7 +28,8 @@ {{#if this.load.isIdle}} {{! Drivers — fleetops' rotating tracking-marker matches the operational - live-map's look and animates on heading changes. }} + live-map's look and animates on heading changes. The popup and tooltip + are the live map's own cards, fed from each marker's index-resource card. }} {{#each this.drivers as |driver|}} - -
-
- {{driver.name}} -
-
-
{{or driver.name "Driver"}}
-
- {{#if driver.current_order_uuid}} - On order - {{else if driver.online}} - Online - {{else}} - Offline - {{/if}} -
-
{{driver.lat}}, {{driver.lng}}
-
-
+ + -
{{or driver.name "Driver"}}
+
{{/each}} @@ -77,27 +61,11 @@ @draggable={{false}} as |marker| > - -
-
- {{vehicle.name}} -
-
-
{{or vehicle.name "Vehicle"}}
- {{#if vehicle.plate_number}} -
{{vehicle.plate_number}}
- {{/if}} - {{#if vehicle.driver_name}} -
- {{vehicle.driver_name}} -
- {{/if}} -
{{vehicle.lat}}, {{vehicle.lng}}
-
-
+ + -
{{or vehicle.name "Vehicle"}}
+
{{/each}} diff --git a/addon/controllers/analytics/reports/index.js b/addon/controllers/analytics/reports/index.js index 489324109..56052e63c 100644 --- a/addon/controllers/analytics/reports/index.js +++ b/addon/controllers/analytics/reports/index.js @@ -29,6 +29,7 @@ export default class AnalyticsReportsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.reportActions.transition.create, + permission: 'iam create report', }, ]; } @@ -39,6 +40,7 @@ export default class AnalyticsReportsIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.reportActions.bulkDelete, + permission: 'iam delete report', }, ]; } @@ -81,10 +83,12 @@ export default class AnalyticsReportsIndexController extends Controller { { label: 'View report...', fn: this.reportActions.transition.view, + permission: 'iam view report', }, { label: 'Edit report...', fn: this.reportActions.transition.edit, + permission: 'iam update report', }, { separator: true, @@ -92,6 +96,7 @@ export default class AnalyticsReportsIndexController extends Controller { { label: 'Delete report...', fn: this.reportActions.delete, + permission: 'iam delete report', }, ], sortable: false, diff --git a/addon/controllers/connectivity/devices/index.js b/addon/controllers/connectivity/devices/index.js index dcb7c2536..b971d6681 100644 --- a/addon/controllers/connectivity/devices/index.js +++ b/addon/controllers/connectivity/devices/index.js @@ -65,12 +65,14 @@ export default class ConnectivityDevicesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.deviceActions.transition.create, + permission: 'fleet-ops create device', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.deviceActions.import, + permission: 'fleet-ops import device', }, { text: this.intl.t('common.export'), @@ -78,6 +80,7 @@ export default class ConnectivityDevicesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.deviceActions.export, + permission: 'fleet-ops export device', }, ]; @@ -87,6 +90,7 @@ export default class ConnectivityDevicesIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.deviceActions.bulkDelete, + permission: 'fleet-ops delete device', }, ]; diff --git a/addon/controllers/connectivity/fuel-providers/index.js b/addon/controllers/connectivity/fuel-providers/index.js index f0ef5a0c8..5b4ffe9fb 100644 --- a/addon/controllers/connectivity/fuel-providers/index.js +++ b/addon/controllers/connectivity/fuel-providers/index.js @@ -40,6 +40,7 @@ export default class ConnectivityFuelProvidersIndexController extends Controller text: 'Connect Integration', type: 'primary', onClick: () => this.fuelIntegrationActions.transition.create(), + permission: 'fleet-ops create fuel-provider-connection', }, ]; } @@ -51,6 +52,7 @@ export default class ConnectivityFuelProvidersIndexController extends Controller { label: `Sync ${selected.length} selected`, fn: () => selected.forEach((connection) => this.syncConnection(connection)), + permission: 'fleet-ops sync fuel-provider-connection', }, ]; } @@ -125,11 +127,11 @@ export default class ConnectivityFuelProvidersIndexController extends Controller sticky: 'right', width: 60, actions: [ - { label: 'Open Integration', fn: this.openConnection }, - { label: 'Edit Settings', fn: this.editConnection }, + { label: 'Open Integration', fn: this.openConnection, permission: 'fleet-ops view fuel-provider-connection' }, + { label: 'Edit Settings', fn: this.editConnection, permission: 'fleet-ops update fuel-provider-connection' }, { separator: true }, - { label: 'Test Connection', fn: this.testConnection }, - { label: 'Sync Transactions', fn: this.syncConnection }, + { label: 'Test Connection', fn: this.testConnection, permission: 'fleet-ops update fuel-provider-connection' }, + { label: 'Sync Transactions', fn: this.syncConnection, permission: 'fleet-ops sync fuel-provider-connection' }, ], sortable: false, filterable: false, diff --git a/addon/controllers/connectivity/sensors/index.js b/addon/controllers/connectivity/sensors/index.js index 4ed9390a1..492a537d1 100644 --- a/addon/controllers/connectivity/sensors/index.js +++ b/addon/controllers/connectivity/sensors/index.js @@ -92,12 +92,14 @@ export default class ConnectivitySensorsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.sensorActions.transition.create, + permission: 'fleet-ops create sensor', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.sensorActions.import, + permission: 'fleet-ops import sensor', }, { text: this.intl.t('common.export'), @@ -105,6 +107,7 @@ export default class ConnectivitySensorsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.sensorActions.export, + permission: 'fleet-ops export sensor', }, ]; @@ -114,6 +117,7 @@ export default class ConnectivitySensorsIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.sensorActions.bulkDelete, + permission: 'fleet-ops delete sensor', }, ]; diff --git a/addon/controllers/connectivity/telematics/index.js b/addon/controllers/connectivity/telematics/index.js index 37369823a..c44ba0556 100644 --- a/addon/controllers/connectivity/telematics/index.js +++ b/addon/controllers/connectivity/telematics/index.js @@ -38,12 +38,14 @@ export default class ConnectivityTelematicsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.telematicActions.transition.create, + permission: 'fleet-ops create telematic', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.telematicActions.import, + permission: 'fleet-ops import telematic', }, { text: this.intl.t('common.export'), @@ -51,6 +53,7 @@ export default class ConnectivityTelematicsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.telematicActions.export, + permission: 'fleet-ops export telematic', }, ]; @@ -60,6 +63,7 @@ export default class ConnectivityTelematicsIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.telematicActions.bulkDelete, + permission: 'fleet-ops delete telematic', }, ]; diff --git a/addon/controllers/maintenance/equipment/index.js b/addon/controllers/maintenance/equipment/index.js index 7c020cfb7..d0a95dac0 100644 --- a/addon/controllers/maintenance/equipment/index.js +++ b/addon/controllers/maintenance/equipment/index.js @@ -46,14 +46,21 @@ export default class MaintenanceEquipmentIndexController extends Controller { helpText: 'Change the layout', }, { 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 }, + { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.equipmentActions.transition.create, permission: 'fleet-ops create equipment' }, + { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.equipmentActions.import, permission: 'fleet-ops import equipment' }, + { + text: this.intl.t('common.export'), + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + wrapperClass: 'hidden md:flex', + onClick: this.equipmentActions.export, + permission: 'fleet-ops export equipment', + }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.equipmentActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.equipmentActions.bulkDelete, permission: 'fleet-ops delete equipment' }]; } get columns() { diff --git a/addon/controllers/maintenance/inspection-forms/index.js b/addon/controllers/maintenance/inspection-forms/index.js index 53d812903..f7fb1c0ff 100644 --- a/addon/controllers/maintenance/inspection-forms/index.js +++ b/addon/controllers/maintenance/inspection-forms/index.js @@ -17,12 +17,12 @@ export default class MaintenanceInspectionFormsIndexController extends Controlle get actionButtons() { return [ { icon: 'refresh', onClick: this.inspectionFormActions.refresh, helpText: this.intl.t('common.refresh') }, - { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.inspectionFormActions.transition.create }, + { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.inspectionFormActions.transition.create, permission: 'fleet-ops create inspection-form' }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.inspectionFormActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.inspectionFormActions.bulkDelete, permission: 'fleet-ops delete inspection-form' }]; } get columns() { diff --git a/addon/controllers/maintenance/inspection-submissions/index.js b/addon/controllers/maintenance/inspection-submissions/index.js index 888898dc5..0cfa6bd70 100644 --- a/addon/controllers/maintenance/inspection-submissions/index.js +++ b/addon/controllers/maintenance/inspection-submissions/index.js @@ -23,12 +23,18 @@ export default class MaintenanceInspectionSubmissionsIndexController extends Con get actionButtons() { return [ { icon: 'refresh', onClick: this.inspectionSubmissionActions.refresh, helpText: this.intl.t('common.refresh') }, - { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.inspectionSubmissionActions.transition.create }, + { + text: this.intl.t('common.new'), + type: 'primary', + icon: 'plus', + onClick: this.inspectionSubmissionActions.transition.create, + permission: 'fleet-ops create inspection-submission', + }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.inspectionSubmissionActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.inspectionSubmissionActions.bulkDelete, permission: 'fleet-ops delete inspection-submission' }]; } get columns() { diff --git a/addon/controllers/maintenance/maintenances/index.js b/addon/controllers/maintenance/maintenances/index.js index 88d817048..a0d75c580 100644 --- a/addon/controllers/maintenance/maintenances/index.js +++ b/addon/controllers/maintenance/maintenances/index.js @@ -31,12 +31,14 @@ export default class MaintenanceMaintenancesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.maintenanceActions.transition.create, + permission: 'fleet-ops create maintenance', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.maintenanceActions.import, + permission: 'fleet-ops import maintenance', }, { text: this.intl.t('common.export'), @@ -44,6 +46,7 @@ export default class MaintenanceMaintenancesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.maintenanceActions.export, + permission: 'fleet-ops export maintenance', }, ]; } @@ -55,6 +58,7 @@ export default class MaintenanceMaintenancesIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.maintenanceActions.bulkDelete, + permission: 'fleet-ops delete maintenance', }, ]; } diff --git a/addon/controllers/maintenance/parts/index.js b/addon/controllers/maintenance/parts/index.js index 3a44dfb5e..55bfc41c2 100644 --- a/addon/controllers/maintenance/parts/index.js +++ b/addon/controllers/maintenance/parts/index.js @@ -46,14 +46,21 @@ export default class MaintenancePartsIndexController extends Controller { helpText: 'Change the layout', }, { 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 }, + { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.partActions.transition.create, permission: 'fleet-ops create part' }, + { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.partActions.import, permission: 'fleet-ops import part' }, + { + text: this.intl.t('common.export'), + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + wrapperClass: 'hidden md:flex', + onClick: this.partActions.export, + permission: 'fleet-ops export part', + }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.partActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.partActions.bulkDelete, permission: 'fleet-ops delete part' }]; } get columns() { diff --git a/addon/controllers/maintenance/schedules/index.js b/addon/controllers/maintenance/schedules/index.js index 094af86ef..16f4b098f 100644 --- a/addon/controllers/maintenance/schedules/index.js +++ b/addon/controllers/maintenance/schedules/index.js @@ -151,14 +151,27 @@ export default class MaintenanceSchedulesIndexController extends Controller { helpText: this.intl.t('common.change-layout'), }, { icon: 'refresh', onClick: this.maintenanceScheduleActions.refresh, helpText: this.intl.t('common.refresh') }, - { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.maintenanceScheduleActions.transition.create }, - { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.maintenanceScheduleActions.import }, - { text: this.intl.t('common.export'), icon: 'long-arrow-up', iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.maintenanceScheduleActions.export }, + { + text: this.intl.t('common.new'), + type: 'primary', + icon: 'plus', + onClick: this.maintenanceScheduleActions.transition.create, + permission: 'fleet-ops create maintenance-schedule', + }, + { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.maintenanceScheduleActions.import, permission: 'fleet-ops import maintenance-schedule' }, + { + text: this.intl.t('common.export'), + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + wrapperClass: 'hidden md:flex', + onClick: this.maintenanceScheduleActions.export, + permission: 'fleet-ops export maintenance-schedule', + }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.maintenanceScheduleActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.maintenanceScheduleActions.bulkDelete, permission: 'fleet-ops delete maintenance-schedule' }]; } get columns() { diff --git a/addon/controllers/maintenance/work-orders/index.js b/addon/controllers/maintenance/work-orders/index.js index fbc789696..082cb8ead 100644 --- a/addon/controllers/maintenance/work-orders/index.js +++ b/addon/controllers/maintenance/work-orders/index.js @@ -21,14 +21,21 @@ export default class MaintenanceWorkOrdersIndexController extends Controller { get actionButtons() { return [ { 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 }, + { text: this.intl.t('common.new'), type: 'primary', icon: 'plus', onClick: this.workOrderActions.transition.create, permission: 'fleet-ops create work-order' }, + { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.workOrderActions.import, permission: 'fleet-ops import work-order' }, + { + text: this.intl.t('common.export'), + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + wrapperClass: 'hidden md:flex', + onClick: this.workOrderActions.export, + permission: 'fleet-ops export work-order', + }, ]; } get bulkActions() { - return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.workOrderActions.bulkDelete }]; + return [{ label: 'Delete selected...', class: 'text-red-500', fn: this.workOrderActions.bulkDelete, permission: 'fleet-ops delete work-order' }]; } get columns() { diff --git a/addon/controllers/management/contacts/index.js b/addon/controllers/management/contacts/index.js index 0756dd08e..056a1a696 100644 --- a/addon/controllers/management/contacts/index.js +++ b/addon/controllers/management/contacts/index.js @@ -62,12 +62,14 @@ export default class ManagementContactsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.contactActions.transition.create, + permission: 'fleet-ops create contact', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.contactActions.import, + permission: 'fleet-ops import contact', }, { text: this.intl.t('common.export'), @@ -75,6 +77,7 @@ export default class ManagementContactsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.contactActions.export, + permission: 'fleet-ops export contact', }, ]; } @@ -88,6 +91,7 @@ export default class ManagementContactsIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.contactActions.bulkDelete, + permission: 'fleet-ops delete contact', }, ]; } diff --git a/addon/controllers/management/drivers/index.js b/addon/controllers/management/drivers/index.js index 9762122d6..30612981f 100644 --- a/addon/controllers/management/drivers/index.js +++ b/addon/controllers/management/drivers/index.js @@ -103,12 +103,14 @@ export default class ManagementDriversIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.driverActions.transition.create, + permission: 'fleet-ops create driver', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.driverActions.import, + permission: 'fleet-ops import driver', }, { text: this.intl.t('common.export'), @@ -116,6 +118,7 @@ export default class ManagementDriversIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.driverActions.export, + permission: 'fleet-ops export driver', }, ]; } @@ -129,6 +132,7 @@ export default class ManagementDriversIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.driverActions.bulkDelete, + permission: 'fleet-ops delete driver', }, ]; } diff --git a/addon/controllers/management/fleets/index.js b/addon/controllers/management/fleets/index.js index 910dbaea3..80f6d84ae 100644 --- a/addon/controllers/management/fleets/index.js +++ b/addon/controllers/management/fleets/index.js @@ -42,12 +42,14 @@ export default class ManagementFleetsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.fleetActions.transition.create, + permission: 'fleet-ops create fleet', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.fleetActions.import, + permission: 'fleet-ops import fleet', }, { text: this.intl.t('common.export'), @@ -55,6 +57,7 @@ export default class ManagementFleetsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.fleetActions.export, + permission: 'fleet-ops export fleet', }, ]; } @@ -68,6 +71,7 @@ export default class ManagementFleetsIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.fleetActions.bulkDelete, + permission: 'fleet-ops delete fleet', }, ]; } diff --git a/addon/controllers/management/fuel-reports/index.js b/addon/controllers/management/fuel-reports/index.js index 1374cfcaf..cdf74dfbe 100644 --- a/addon/controllers/management/fuel-reports/index.js +++ b/addon/controllers/management/fuel-reports/index.js @@ -56,12 +56,14 @@ export default class ManagementFuelReportsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.fuelReportActions.transition.create, + permission: 'fleet-ops create fuel-report', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.fuelReportActions.import, + permission: 'fleet-ops import fuel-report', }, { text: this.intl.t('common.export'), @@ -69,6 +71,7 @@ export default class ManagementFuelReportsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.fuelReportActions.export, + permission: 'fleet-ops export fuel-report', }, ]; } @@ -82,6 +85,7 @@ export default class ManagementFuelReportsIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.fuelReportActions.bulkDelete, + permission: 'fleet-ops delete fuel-report', }, ]; } diff --git a/addon/controllers/management/fuel-transactions/index.js b/addon/controllers/management/fuel-transactions/index.js index e6873351a..30e940e3e 100644 --- a/addon/controllers/management/fuel-transactions/index.js +++ b/addon/controllers/management/fuel-transactions/index.js @@ -71,6 +71,7 @@ export default class ManagementFuelTransactionsIndexController extends Controlle icon: 'gas-pump', text: 'Fuel Integrations', onClick: () => this.hostRouter.transitionTo('console.fleet-ops.connectivity.fuel-providers.index'), + permission: 'fleet-ops list fuel-provider-connection', }, ]; } @@ -86,6 +87,7 @@ export default class ManagementFuelTransactionsIndexController extends Controlle { label: `Reprocess ${selected.length} selected`, fn: () => this.confirmAction('reprocess', selected), + permission: 'fleet-ops update fuel-provider-transaction', }, ]; } @@ -196,14 +198,14 @@ export default class ManagementFuelTransactionsIndexController extends Controlle sticky: 'right', width: 60, actions: [ - { label: 'Review Details', fn: this.openDetails }, - { label: 'Open Fuel Report', fn: this.openFuelReport, isVisible: (transaction) => Boolean(transaction.fuel_report_id) }, + { label: 'Review Details', fn: this.openDetails, permission: 'fleet-ops view fuel-provider-transaction' }, + { label: 'Open Fuel Report', fn: this.openFuelReport, isVisible: (transaction) => Boolean(transaction.fuel_report_id), permission: 'fleet-ops view fuel-report' }, { separator: true }, - { label: 'Match to Vehicle', fn: this.matchVehicle }, - { label: 'Match to Order', fn: this.matchOrder }, - { label: 'Reprocess / Rematch', fn: (transaction) => this.confirmAction('reprocess', transaction) }, - { label: 'Ignore Transaction', fn: (transaction) => this.confirmAction('ignored', transaction) }, - { label: 'Mark Reviewed', fn: (transaction) => this.confirmAction('reviewed', transaction) }, + { label: 'Match to Vehicle', fn: this.matchVehicle, permission: 'fleet-ops update fuel-provider-transaction' }, + { label: 'Match to Order', fn: this.matchOrder, permission: 'fleet-ops update fuel-provider-transaction' }, + { label: 'Reprocess / Rematch', fn: (transaction) => this.confirmAction('reprocess', transaction), permission: 'fleet-ops update fuel-provider-transaction' }, + { label: 'Ignore Transaction', fn: (transaction) => this.confirmAction('ignored', transaction), permission: 'fleet-ops review fuel-provider-transaction' }, + { label: 'Mark Reviewed', fn: (transaction) => this.confirmAction('reviewed', transaction), permission: 'fleet-ops review fuel-provider-transaction' }, ], sortable: false, filterable: false, diff --git a/addon/controllers/management/issues/index.js b/addon/controllers/management/issues/index.js index 9c3cfaecd..5774b8865 100644 --- a/addon/controllers/management/issues/index.js +++ b/addon/controllers/management/issues/index.js @@ -56,12 +56,14 @@ export default class ManagementIssuesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.issueActions.transition.create, + permission: 'fleet-ops create issue', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.issueActions.import, + permission: 'fleet-ops import issue', }, { text: this.intl.t('common.export'), @@ -69,6 +71,7 @@ export default class ManagementIssuesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.issueActions.export, + permission: 'fleet-ops export issue', }, ]; } @@ -82,6 +85,7 @@ export default class ManagementIssuesIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.issueActions.bulkDelete, + permission: 'fleet-ops delete issue', }, ]; } diff --git a/addon/controllers/management/places/index.js b/addon/controllers/management/places/index.js index f4236ea31..6fdc416b4 100644 --- a/addon/controllers/management/places/index.js +++ b/addon/controllers/management/places/index.js @@ -34,12 +34,14 @@ export default class ManagementPlacesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.placeActions.transition.create, + permission: 'fleet-ops create place', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.placeActions.import, + permission: 'fleet-ops import place', }, { text: this.intl.t('common.export'), @@ -47,6 +49,7 @@ export default class ManagementPlacesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.placeActions.export, + permission: 'fleet-ops export place', }, ]; } @@ -60,6 +63,7 @@ export default class ManagementPlacesIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.placeActions.bulkDelete, + permission: 'fleet-ops delete place', }, ]; } diff --git a/addon/controllers/management/vehicles/index.js b/addon/controllers/management/vehicles/index.js index 1662a134a..1e63fbf4d 100644 --- a/addon/controllers/management/vehicles/index.js +++ b/addon/controllers/management/vehicles/index.js @@ -102,12 +102,14 @@ export default class ManagementVehiclesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.vehicleActions.transition.create, + permission: 'fleet-ops create vehicle', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.vehicleActions.import, + permission: 'fleet-ops import vehicle', }, { text: this.intl.t('common.export'), @@ -115,6 +117,7 @@ export default class ManagementVehiclesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.vehicleActions.export, + permission: 'fleet-ops export vehicle', }, ]; } @@ -128,6 +131,7 @@ export default class ManagementVehiclesIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.vehicleActions.bulkDelete, + permission: 'fleet-ops delete vehicle', }, ]; } diff --git a/addon/controllers/management/vendors/index.js b/addon/controllers/management/vendors/index.js index 03d941bbe..e76a713ee 100644 --- a/addon/controllers/management/vendors/index.js +++ b/addon/controllers/management/vendors/index.js @@ -83,12 +83,14 @@ export default class ManagementVendorsIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.vendorActions.transition.create, + permission: 'fleet-ops create vendor', }, { text: this.intl.t('common.import'), type: 'magic', icon: 'upload', onClick: this.vendorActions.import, + permission: 'fleet-ops import vendor', }, { text: this.intl.t('common.export'), @@ -96,6 +98,7 @@ export default class ManagementVendorsIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.vendorActions.export, + permission: 'fleet-ops export vendor', }, ]; } @@ -109,6 +112,7 @@ export default class ManagementVendorsIndexController extends Controller { label: this.intl.t('common.delete-selected-count', { count: selected.length }), class: 'text-red-500', fn: this.vendorActions.bulkDelete, + permission: 'fleet-ops delete vendor', }, ]; } diff --git a/addon/controllers/operations/orders/index.js b/addon/controllers/operations/orders/index.js index 5b40c7bc8..ab9563a89 100644 --- a/addon/controllers/operations/orders/index.js +++ b/addon/controllers/operations/orders/index.js @@ -86,6 +86,7 @@ export default class OperationsOrdersIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.orderActions.transition.create, + permission: 'fleet-ops create order', }, { text: this.intl.t('common.export'), @@ -93,12 +94,14 @@ export default class OperationsOrdersIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.orderActions.export, + permission: 'fleet-ops export order', }, { text: this.intl.t('common.import'), icon: 'file-import', wrapperClass: 'hidden md:flex', onClick: () => this.orderActions.importOrders({ onImportComplete: this.orderActions.refresh }), + permission: 'fleet-ops import order', }, ]; } @@ -110,23 +113,27 @@ export default class OperationsOrdersIndexController extends Controller { label: this.intl.t('common.cancel-resource', { resource: this.intl.t('resource.orders') }), icon: 'ban', fn: this.orderActions.bulkCancel, + permission: 'fleet-ops cancel order', }, { label: this.intl.t('common.delete-resource', { resource: this.intl.t('resource.orders') }), icon: 'trash', class: 'text-red-500', fn: this.orderActions.bulkDelete, + permission: 'fleet-ops delete order', }, { separator: true }, { label: this.intl.t('common.dispatch-orders'), icon: 'rocket', fn: this.orderActions.bulkDispatch, + permission: 'fleet-ops dispatch order', }, { label: this.intl.t('common.assign-drivers'), icon: 'user-plus', fn: this.orderActions.bulkAssignDriver, + permission: 'fleet-ops assign-driver-for order', }, ]; } diff --git a/addon/controllers/operations/orders/index/details.js b/addon/controllers/operations/orders/index/details.js index 188ec9256..333846cc9 100644 --- a/addon/controllers/operations/orders/index/details.js +++ b/addon/controllers/operations/orders/index/details.js @@ -17,6 +17,7 @@ export default class OperationsOrdersIndexDetailsController extends Controller { @service hostRouter; @service universe; @service sidebar; + @service abilities; @tracked routingControl; @tracked routingCompleted = false; @tracked realtimeOrderPublicId = null; @@ -90,12 +91,14 @@ export default class OperationsOrdersIndexDetailsController extends Controller { { text: 'Edit details', icon: 'pencil', + permission: 'fleet-ops update order', disabled: this.model.status === 'canceled', fn: () => this.orderActions.editOrderDetails(this.model), }, { text: 'Update activity', icon: 'signal', + permission: 'fleet-ops update order', disabled: this.model.status === 'canceled', fn: () => this.orderActions.updateActivity(this.model, { @@ -105,12 +108,14 @@ export default class OperationsOrdersIndexDetailsController extends Controller { { text: this.model.has_driver_assigned ? 'Unassign driver' : 'Assign driver', icon: this.model.has_driver_assigned ? 'user-xmark' : 'edit', + permission: 'fleet-ops assign-driver-for order', disabled: this.model.has_driver_assigned ? !this.model.hasActiveStatus || !this.model.driver_assigned : !this.model.hasActiveStatus, fn: () => (this.model.has_driver_assigned ? this.orderActions.unassignDriver(this.model) : this.orderActions.assignDriver(this.model)), }, { text: 'View order label', icon: 'file-invoice', + permission: 'fleet-ops view order', fn: () => this.orderActions.viewLabel(this.model), }, { @@ -133,6 +138,7 @@ export default class OperationsOrdersIndexDetailsController extends Controller { text: 'Cancel order', icon: 'ban', class: 'text-danger', + permission: 'fleet-ops cancel order', disabled: this.model.status === 'canceled', fn: () => this.orderActions.cancel(this.model), }, @@ -140,6 +146,7 @@ export default class OperationsOrdersIndexDetailsController extends Controller { text: 'Delete order', icon: 'trash', class: 'text-danger', + permission: 'fleet-ops delete order', fn: () => this.orderActions.delete(this.model, { taskOptions: { @@ -151,7 +158,29 @@ export default class OperationsOrdersIndexDetailsController extends Controller { }, ].filter(Boolean), }, - ]; + ].map((actionButton) => ({ ...actionButton, items: this.permittedMenuItems(actionButton.items) })); + } + + /** + * The panel header dropdown renders `items` as-is and ignores `permission`, so items the + * user is not permitted to use are removed here, along with any separators left dangling. + */ + permittedMenuItems(items = []) { + const permitted = items.filter((item) => !item.permission || this.abilities.can(item.permission)); + const result = permitted.reduce((list, item) => { + if (item.separator && (list.length === 0 || list[list.length - 1].separator)) { + return list; + } + + list.push(item); + return list; + }, []); + + if (result.length && result[result.length - 1].separator) { + result.pop(); + } + + return result; } @action handleActivityModalFinish(options) { diff --git a/addon/controllers/operations/service-rates/index.js b/addon/controllers/operations/service-rates/index.js index cdb91974f..52413130a 100644 --- a/addon/controllers/operations/service-rates/index.js +++ b/addon/controllers/operations/service-rates/index.js @@ -29,6 +29,7 @@ export default class OperationsServiceRatesIndexController extends Controller { type: 'primary', icon: 'plus', onClick: this.serviceRateActions.transition.create, + permission: 'fleet-ops create service-rate', }, { text: this.intl.t('common.export'), @@ -36,6 +37,7 @@ export default class OperationsServiceRatesIndexController extends Controller { iconClass: 'rotate-icon-45', wrapperClass: 'hidden md:flex', onClick: this.serviceRateActions.export, + permission: 'fleet-ops export service-rate', }, ]; } @@ -47,6 +49,7 @@ export default class OperationsServiceRatesIndexController extends Controller { label: 'Delete selected...', class: 'text-red-500', fn: this.serviceRateActions.bulkDelete, + permission: 'fleet-ops delete service-rate', }, ]; } diff --git a/addon/controllers/settings/index.js b/addon/controllers/settings/index.js index 9ec9284b8..586754c08 100644 --- a/addon/controllers/settings/index.js +++ b/addon/controllers/settings/index.js @@ -28,6 +28,12 @@ export default class SettingsIndexController extends Controller { description: 'Keep commerce, metadata, and visual conventions aligned.', links: [ { label: 'Payments', route: 'settings.payments', icon: 'cash-register', description: 'Payment setup for operational commerce workflows.' }, + { + label: 'Telematics', + route: 'settings.telematics', + icon: 'satellite-dish', + description: 'Device event and position history preferences within the system retention policy.', + }, { label: 'Custom Fields', route: 'settings.custom-fields', icon: 'pen-to-square', description: 'Operational metadata fields for Fleet-Ops records.' }, { label: 'Avatars', route: 'settings.avatars', icon: 'icons', description: 'Visual assets for driver, vehicle, and map displays.' }, ], @@ -113,6 +119,13 @@ export default class SettingsIndexController extends Controller { title: 'Scheduling settings', description: 'Manage schedule templates and timing rules for planned work.', }, + { + label: 'Telematics', + icon: 'satellite-dish', + slug: 'fleet-ops/settings/telematics', + title: 'Telematics Settings', + description: 'Choose how long your organization keeps device events and position history.', + }, { label: 'Custom Fields', icon: 'pen-to-square', diff --git a/addon/controllers/settings/telematics.js b/addon/controllers/settings/telematics.js new file mode 100644 index 000000000..9700fe77c --- /dev/null +++ b/addon/controllers/settings/telematics.js @@ -0,0 +1,113 @@ +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'; + +/** Organization history preferences, subject to the system retention policy. */ +export default class SettingsTelematicsController extends Controller { + @service fetch; + @service notifications; + @service intl; + + @tracked eventRetentionDays = 30; + @tracked positionRetentionDays = 90; + @tracked useDefaultEventRetention = true; + @tracked useDefaultPositionRetention = true; + @tracked defaults = {}; + @tracked policy = {}; + @tracked settingsLoaded = false; + + get eventRetentionMinimum() { + return this.policy.max_event_retention_days > 0 ? 1 : 0; + } + + get eventRetentionMaximum() { + return this.policy.max_event_retention_days > 0 ? this.policy.max_event_retention_days : 3650; + } + + get positionRetentionMinimum() { + return this.policy.max_position_retention_days > 0 ? 1 : 0; + } + + get positionRetentionMaximum() { + return this.policy.max_position_retention_days > 0 ? this.policy.max_position_retention_days : 3650; + } + + get eventRetentionPolicyHelp() { + return this.retentionPolicyHelp(this.policy.max_event_retention_days); + } + + get positionRetentionPolicyHelp() { + return this.retentionPolicyHelp(this.policy.max_position_retention_days); + } + + get settingsPayload() { + return { + event_retention_days: this.useDefaultEventRetention ? null : this.eventRetentionDays, + position_retention_days: this.useDefaultPositionRetention ? null : this.positionRetentionDays, + }; + } + + @task({ restartable: true }) *getSettings() { + this.settingsLoaded = false; + + try { + const settings = yield this.fetch.get('fleet-ops/settings/telematics-settings'); + this.applySettings(settings); + this.settingsLoaded = true; + } catch (error) { + this.notifications.serverError(error); + } + } + + @task({ drop: true }) *saveSettings() { + if (!this.settingsLoaded) { + return; + } + + try { + const settings = yield this.fetch.post('fleet-ops/settings/telematics-settings', this.settingsPayload); + this.applySettings(settings); + this.notifications.success(this.intl.t('settings.telematics.settings-saved')); + } catch (error) { + this.notifications.serverError(error); + } + } + + @action setUseDefaultEventRetention(enabled) { + this.useDefaultEventRetention = enabled; + if (enabled) { + this.eventRetentionDays = this.defaults.event_retention_days ?? this.eventRetentionDays; + } + } + + @action setUseDefaultPositionRetention(enabled) { + this.useDefaultPositionRetention = enabled; + if (enabled) { + this.positionRetentionDays = this.defaults.position_retention_days ?? this.positionRetentionDays; + } + } + + applySettings(settings = {}) { + const has = (object, key) => Object.prototype.hasOwnProperty.call(object ?? {}, key); + const hasHistoryMetadata = ['event_retention_days', 'position_retention_days'].every( + (key) => has(settings?.preferences, key) && has(settings?.defaults, key) && has(settings?.policy, `max_${key}`) + ); + + if (!hasHistoryMetadata) { + throw new Error(this.intl.t('settings.telematics.settings-unavailable')); + } + + this.eventRetentionDays = settings.event_retention_days ?? this.eventRetentionDays; + this.positionRetentionDays = settings.position_retention_days ?? this.positionRetentionDays; + this.useDefaultEventRetention = (settings.preferences?.event_retention_days ?? null) === null; + this.useDefaultPositionRetention = (settings.preferences?.position_retention_days ?? null) === null; + this.defaults = settings.defaults ?? this.defaults; + this.policy = settings.policy ?? this.policy; + } + + retentionPolicyHelp(maximum) { + return maximum > 0 ? this.intl.t('settings.telematics.history-policy-maximum', { days: maximum }) : this.intl.t('settings.telematics.history-policy-unlimited'); + } +} diff --git a/addon/extension.js b/addon/extension.js index bcf5b4602..5aebbad9c 100644 --- a/addon/extension.js +++ b/addon/extension.js @@ -18,66 +18,77 @@ export default { description: 'Everything across resources, maintenance and staffing that needs a decision today.', icon: 'satellite-dish', route: 'console.fleet-ops.management.index', + permission: 'fleet-ops list driver', }, { title: 'Orders', description: 'Create, dispatch, and track delivery orders in real time.', icon: 'boxes-stacked', route: 'console.fleet-ops.operations.orders', + permission: 'fleet-ops list order', }, { title: 'Places', description: 'Manage saved locations, addresses, and points of interest.', icon: 'location-dot', route: 'console.fleet-ops.management.places', + permission: 'fleet-ops list place', }, { title: 'Drivers', description: 'Manage driver profiles, assignments, and live locations.', icon: 'id-card', route: 'console.fleet-ops.management.drivers', + permission: 'fleet-ops list driver', }, { title: 'Vehicles', description: 'View and manage your vehicle fleet and telematics.', icon: 'truck', route: 'console.fleet-ops.management.vehicles', + permission: 'fleet-ops list vehicle', }, { title: intlService.t('menu.trailers'), description: intlService.t('trailer.navigation-description'), icon: 'trailer', route: 'console.fleet-ops.management.trailers', + permission: 'fleet-ops list trailer', }, { title: 'Fleets', description: 'Organise drivers and vehicles into operational fleets.', icon: 'layer-group', route: 'console.fleet-ops.management.fleets', + permission: 'fleet-ops list fleet', }, { title: 'Service Rates', description: 'Configure pricing rules and service rate cards.', icon: 'tags', route: 'console.fleet-ops.operations.service-rates', + permission: 'fleet-ops list service-rate', }, { title: 'Devices', description: 'Manage connected telematics devices and their sensor data.', icon: 'microchip', route: 'console.fleet-ops.connectivity.devices', + permission: 'fleet-ops list device', }, { title: 'Reports', description: 'Generate and review operational analytics reports.', icon: 'chart-bar', route: 'console.fleet-ops.analytics.reports', + permission: 'iam list report', }, { title: 'Orchestrator', description: 'Intelligently allocate and dispatch orders to available drivers.', icon: 'diagram-project', route: 'console.fleet-ops.operations.orchestrator', + permission: 'fleet-ops list order', }, ], }); @@ -101,6 +112,11 @@ export default { icon: 'location-arrow', component: new ExtensionComponent('@fleetbase/fleetops-engine', 'admin/navigator-app'), }), + new MenuItem({ + title: 'Telematics', + icon: 'satellite-dish', + component: new ExtensionComponent('@fleetbase/fleetops-engine', 'admin/telematics-settings'), + }), ], { slug: 'fleet-ops', @@ -161,6 +177,14 @@ export default { }, registerWidgets(widgetService) { + // `order` places a default widget on the default dashboard, which mixes widgets from + // every extension; the grid fills rows first-fit, lowest order first (widgets without + // one follow). The default layout is: + // 10-40 KPI row: Radar, Revenue (ledger, 20), Active Orders, Drivers Online + // 41-44 Ledger KPI row: Expenses, Net Income, Outstanding AR, Overdue AR + // 50 Live Fleet Map, full width + // 60-80 Revenue Trend, Top Drivers, Maintenance Overview, a third of the width each + // 150-170 Ledger Recent Financial Activity + Cash Flow Summary | console Blog + GitHub const widgets = [ // Legacy monolithic 13-tile widget — kept registered for one release as // users have it pinned to existing dashboards. The new KPI tile widgets @@ -186,7 +210,7 @@ export default { component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/kpi-earnings'), grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, category: 'KPI Tiles', - default: true, + default: false, }), new Widget({ id: 'fleet-ops-kpi-aov-widget', @@ -196,7 +220,7 @@ export default { component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/kpi-aov'), grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, category: 'KPI Tiles', - default: true, + default: false, }), new Widget({ id: 'fleet-ops-kpi-distance-widget', @@ -216,6 +240,7 @@ export default { component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/kpi-active-orders'), grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, category: 'KPI Tiles', + order: 30, default: true, }), new Widget({ @@ -226,6 +251,7 @@ export default { component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/kpi-drivers-online'), grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, category: 'KPI Tiles', + order: 40, default: true, }), new Widget({ @@ -236,6 +262,7 @@ export default { component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/radar'), grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, category: 'KPI Tiles', + order: 10, default: true, }), new Widget({ @@ -266,8 +293,9 @@ export default { description: 'Real-time driver positions and active routes.', icon: 'map-location-dot', component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/live-fleet'), - grid_options: { w: 8, h: 11, minW: 8, minH: 8 }, + grid_options: { w: 12, h: 12, minW: 8, minH: 8 }, category: 'Maps', + order: 50, default: true, }), new Widget({ @@ -276,8 +304,9 @@ export default { description: 'Revenue over time with period comparison.', icon: 'chart-line', component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/revenue-trend'), - grid_options: { w: 4, h: 11, minW: 4, minH: 8 }, + grid_options: { w: 4, h: 9, minW: 4, minH: 7 }, category: 'Analytics', + order: 60, default: true, }), new Widget({ @@ -306,8 +335,9 @@ export default { description: 'Driver leaderboard sortable by orders, on-time %, or distance.', icon: 'medal', component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/top-drivers'), - grid_options: { w: 6, h: 6, minW: 5, minH: 5 }, + grid_options: { w: 4, h: 9, minW: 4, minH: 6 }, category: 'Analytics', + order: 70, default: true, }), new Widget({ @@ -346,8 +376,9 @@ export default { description: 'Overdue, scheduled, and YTD maintenance spend.', icon: 'wrench', component: new ExtensionComponent('@fleetbase/fleetops-engine', 'widget/maintenance-overview'), - grid_options: { w: 6, h: 6, minW: 5, minH: 5 }, + grid_options: { w: 4, h: 9, minW: 4, minH: 6 }, category: 'Analytics', + order: 80, default: true, }), new Widget({ @@ -456,6 +487,7 @@ export default { 'fleet-ops:template:settings:routing', 'fleet-ops:template:settings:orchestrator', 'fleet-ops:component:admin:routing-settings', + 'fleet-ops:component:admin:telematics-settings', ]); }, }; diff --git a/addon/routes.js b/addon/routes.js index 3454717b5..9596e2885 100644 --- a/addon/routes.js +++ b/addon/routes.js @@ -323,6 +323,7 @@ export default buildRoutes(function () { this.route('map'); this.route('orchestrator'); this.route('scheduling'); + this.route('telematics'); this.route('payments', function () { this.route('index', { path: '/' }); this.route('onboard'); diff --git a/addon/routes/analytics/index.js b/addon/routes/analytics/index.js index 3d64374e9..7f2d927d2 100644 --- a/addon/routes/analytics/index.js +++ b/addon/routes/analytics/index.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class AnalyticsIndexRoute extends Route {} +export default class AnalyticsIndexRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view analytics')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/analytics/reports.js b/addon/routes/analytics/reports.js index fdd1396cf..da2b0f72f 100644 --- a/addon/routes/analytics/reports.js +++ b/addon/routes/analytics/reports.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class AnalyticsReportsRoute extends Route {} +export default class AnalyticsReportsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('iam list report')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/analytics/reports/index/details.js b/addon/routes/analytics/reports/index/details.js index aafa2b55a..d2d163626 100644 --- a/addon/routes/analytics/reports/index/details.js +++ b/addon/routes/analytics/reports/index/details.js @@ -17,7 +17,7 @@ export default class AnalyticsReportsIndexDetailsRoute extends Route { } beforeModel() { - if (this.abilities.cannot('fleet-ops view report')) { + if (this.abilities.cannot('iam view report')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); return this.hostRouter.transitionTo('console.fleet-ops.analytics.reports.index'); } diff --git a/addon/routes/analytics/reports/index/edit.js b/addon/routes/analytics/reports/index/edit.js index 4ca59d92a..99e510235 100644 --- a/addon/routes/analytics/reports/index/edit.js +++ b/addon/routes/analytics/reports/index/edit.js @@ -17,7 +17,7 @@ export default class AnalyticsReportsIndexEditRoute extends Route { } beforeModel() { - if (this.abilities.cannot('fleet-ops update report')) { + if (this.abilities.cannot('iam update report')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); return this.hostRouter.transitionTo('console.fleet-ops.analytics.reports.index'); } diff --git a/addon/routes/analytics/reports/index/new.js b/addon/routes/analytics/reports/index/new.js index 3bd42eb9d..88d4d9e1f 100644 --- a/addon/routes/analytics/reports/index/new.js +++ b/addon/routes/analytics/reports/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class AnalyticsReportsIndexNewRoute extends Route {} +export default class AnalyticsReportsIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('iam create report')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.analytics.reports.index'); + } + } +} diff --git a/addon/routes/connectivity/devices.js b/addon/routes/connectivity/devices.js index f5a216674..e1fe1539e 100644 --- a/addon/routes/connectivity/devices.js +++ b/addon/routes/connectivity/devices.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityDevicesRoute extends Route {} +export default class ConnectivityDevicesRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list device')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/connectivity/devices/index/new.js b/addon/routes/connectivity/devices/index/new.js index 8c69bb213..450685ce7 100644 --- a/addon/routes/connectivity/devices/index/new.js +++ b/addon/routes/connectivity/devices/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityDevicesIndexNewRoute extends Route {} +export default class ConnectivityDevicesIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create device')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.devices.index'); + } + } +} diff --git a/addon/routes/connectivity/events.js b/addon/routes/connectivity/events.js index 4f6e2b6e1..aaf94573c 100644 --- a/addon/routes/connectivity/events.js +++ b/addon/routes/connectivity/events.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityEventsRoute extends Route {} +export default class ConnectivityEventsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list device-event')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/connectivity/fuel-providers.js b/addon/routes/connectivity/fuel-providers.js index 0d9c95fc7..95897b8f8 100644 --- a/addon/routes/connectivity/fuel-providers.js +++ b/addon/routes/connectivity/fuel-providers.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityFuelProvidersRoute extends Route {} +export default class ConnectivityFuelProvidersRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list fuel-provider-connection')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/connectivity/fuel-providers/details.js b/addon/routes/connectivity/fuel-providers/details.js index 04be2351a..f974a05c8 100644 --- a/addon/routes/connectivity/fuel-providers/details.js +++ b/addon/routes/connectivity/fuel-providers/details.js @@ -6,6 +6,15 @@ export default class ConnectivityFuelProvidersIndexDetailsRoute extends Route { @service store; @service notifications; @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view fuel-provider-connection')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.fuel-providers.index'); + } + } setupController(controller, model) { super.setupController(controller, model); diff --git a/addon/routes/connectivity/fuel-providers/details/transactions.js b/addon/routes/connectivity/fuel-providers/details/transactions.js index 6a457461f..0addb38f7 100644 --- a/addon/routes/connectivity/fuel-providers/details/transactions.js +++ b/addon/routes/connectivity/fuel-providers/details/transactions.js @@ -4,6 +4,18 @@ import { action } from '@ember/object'; export default class FuelIntegrationTransactionsRoute extends Route { @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list fuel-provider-transaction')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.fuel-providers.index'); + } + } + queryParams = Object.fromEntries(['page', 'limit', 'sort', 'query', 'sync_status', 'vehicle', 'transaction_at'].map((key) => [key, { refreshModel: true }])); model(params) { const connection = this.modelFor('connectivity.fuel-providers.details'); diff --git a/addon/routes/connectivity/fuel-providers/edit.js b/addon/routes/connectivity/fuel-providers/edit.js index 8d72929c4..478c81e75 100644 --- a/addon/routes/connectivity/fuel-providers/edit.js +++ b/addon/routes/connectivity/fuel-providers/edit.js @@ -6,6 +6,15 @@ export default class ConnectivityFuelProvidersIndexEditRoute extends Route { @service store; @service notifications; @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops update fuel-provider-connection')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.fuel-providers.index'); + } + } @action error(error) { this.notifications.serverError(error); diff --git a/addon/routes/connectivity/fuel-providers/new.js b/addon/routes/connectivity/fuel-providers/new.js index b3605cce0..f20b9af67 100644 --- a/addon/routes/connectivity/fuel-providers/new.js +++ b/addon/routes/connectivity/fuel-providers/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityFuelProvidersIndexNewRoute extends Route {} +export default class ConnectivityFuelProvidersIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create fuel-provider-connection')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.fuel-providers.index'); + } + } +} diff --git a/addon/routes/connectivity/sensors.js b/addon/routes/connectivity/sensors.js index f3bc70a81..9bc86da27 100644 --- a/addon/routes/connectivity/sensors.js +++ b/addon/routes/connectivity/sensors.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivitySensorsRoute extends Route {} +export default class ConnectivitySensorsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list sensor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/connectivity/sensors/index/new.js b/addon/routes/connectivity/sensors/index/new.js index 836d72730..d23d3cafe 100644 --- a/addon/routes/connectivity/sensors/index/new.js +++ b/addon/routes/connectivity/sensors/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivitySensorsIndexNewRoute extends Route {} +export default class ConnectivitySensorsIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create sensor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.sensors.index'); + } + } +} diff --git a/addon/routes/connectivity/telematics.js b/addon/routes/connectivity/telematics.js index 68fd97c7c..39e33321e 100644 --- a/addon/routes/connectivity/telematics.js +++ b/addon/routes/connectivity/telematics.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityTelematicsRoute extends Route {} +export default class ConnectivityTelematicsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list telematic')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/connectivity/telematics/new.js b/addon/routes/connectivity/telematics/new.js index f993f7b19..0c6d68dd9 100644 --- a/addon/routes/connectivity/telematics/new.js +++ b/addon/routes/connectivity/telematics/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityTelematicsNewRoute extends Route {} +export default class ConnectivityTelematicsNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create telematic')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.connectivity.telematics.index'); + } + } +} diff --git a/addon/routes/connectivity/tracking.js b/addon/routes/connectivity/tracking.js index 81d44b954..de24cdf6f 100644 --- a/addon/routes/connectivity/tracking.js +++ b/addon/routes/connectivity/tracking.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ConnectivityTrackingRoute extends Route {} +export default class ConnectivityTrackingRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list vehicle')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/equipment.js b/addon/routes/maintenance/equipment.js index 3a5bb687a..cec17bfb6 100644 --- a/addon/routes/maintenance/equipment.js +++ b/addon/routes/maintenance/equipment.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceEquipmentRoute extends Route {} +export default class MaintenanceEquipmentRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list equipment')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/equipment/index/details.js b/addon/routes/maintenance/equipment/index/details.js index 43035ca49..7f8efa9b7 100644 --- a/addon/routes/maintenance/equipment/index/details.js +++ b/addon/routes/maintenance/equipment/index/details.js @@ -12,14 +12,14 @@ export default class MaintenanceEquipmentIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.equipment.index'); + 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('maintenance.equipment.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); } } diff --git a/addon/routes/maintenance/equipment/index/edit.js b/addon/routes/maintenance/equipment/index/edit.js index 136ea947c..51e06996b 100644 --- a/addon/routes/maintenance/equipment/index/edit.js +++ b/addon/routes/maintenance/equipment/index/edit.js @@ -12,14 +12,14 @@ export default class MaintenanceEquipmentIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.equipment.index'); + 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('maintenance.equipment.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); } } diff --git a/addon/routes/maintenance/equipment/index/new.js b/addon/routes/maintenance/equipment/index/new.js index 965997967..aee969df0 100644 --- a/addon/routes/maintenance/equipment/index/new.js +++ b/addon/routes/maintenance/equipment/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceEquipmentIndexNewRoute extends Route {} +export default class MaintenanceEquipmentIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create equipment')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.equipment.index'); + } + } +} diff --git a/addon/routes/maintenance/index.js b/addon/routes/maintenance/index.js index 04e2df618..767e3e292 100644 --- a/addon/routes/maintenance/index.js +++ b/addon/routes/maintenance/index.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceIndexRoute extends Route {} +export default class MaintenanceIndexRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list work-order')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/inspection-forms.js b/addon/routes/maintenance/inspection-forms.js index 97a8c467a..b427120f6 100644 --- a/addon/routes/maintenance/inspection-forms.js +++ b/addon/routes/maintenance/inspection-forms.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceInspectionFormsRoute extends Route {} +export default class MaintenanceInspectionFormsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list inspection-form')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/inspection-forms/index/details.js b/addon/routes/maintenance/inspection-forms/index/details.js index 202e83766..25f0e46b6 100644 --- a/addon/routes/maintenance/inspection-forms/index/details.js +++ b/addon/routes/maintenance/inspection-forms/index/details.js @@ -6,6 +6,15 @@ export default class MaintenanceInspectionFormsIndexDetailsRoute extends Route { @service store; @service hostRouter; @service notifications; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view inspection-form')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'); + } + } model({ public_id }) { return this.store.findRecord('inspection-form', public_id); @@ -13,6 +22,6 @@ export default class MaintenanceInspectionFormsIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); - return this.hostRouter.transitionTo('maintenance.inspection-forms.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'); } } diff --git a/addon/routes/maintenance/inspection-forms/index/edit.js b/addon/routes/maintenance/inspection-forms/index/edit.js index f8b15d6dd..531d9819c 100644 --- a/addon/routes/maintenance/inspection-forms/index/edit.js +++ b/addon/routes/maintenance/inspection-forms/index/edit.js @@ -6,6 +6,15 @@ export default class MaintenanceInspectionFormsIndexEditRoute extends Route { @service store; @service hostRouter; @service notifications; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops update inspection-form')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'); + } + } model({ public_id }) { return this.store.findRecord('inspection-form', public_id); @@ -13,6 +22,6 @@ export default class MaintenanceInspectionFormsIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); - return this.hostRouter.transitionTo('maintenance.inspection-forms.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'); } } diff --git a/addon/routes/maintenance/inspection-forms/index/new.js b/addon/routes/maintenance/inspection-forms/index/new.js index 96a813892..b4867f549 100644 --- a/addon/routes/maintenance/inspection-forms/index/new.js +++ b/addon/routes/maintenance/inspection-forms/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceInspectionFormsIndexNewRoute extends Route {} +export default class MaintenanceInspectionFormsIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create inspection-form')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-forms.index'); + } + } +} diff --git a/addon/routes/maintenance/inspection-submissions.js b/addon/routes/maintenance/inspection-submissions.js index 73b235dce..028acfb78 100644 --- a/addon/routes/maintenance/inspection-submissions.js +++ b/addon/routes/maintenance/inspection-submissions.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceInspectionSubmissionsRoute extends Route {} +export default class MaintenanceInspectionSubmissionsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list inspection-submission')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/inspection-submissions/index/details.js b/addon/routes/maintenance/inspection-submissions/index/details.js index ebb148013..faa1cddbd 100644 --- a/addon/routes/maintenance/inspection-submissions/index/details.js +++ b/addon/routes/maintenance/inspection-submissions/index/details.js @@ -6,6 +6,15 @@ export default class MaintenanceInspectionSubmissionsIndexDetailsRoute extends R @service store; @service hostRouter; @service notifications; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view inspection-submission')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'); + } + } model({ public_id }) { return this.store.findRecord('inspection-submission', public_id); @@ -13,6 +22,6 @@ export default class MaintenanceInspectionSubmissionsIndexDetailsRoute extends R @action error(error) { this.notifications.serverError(error); - return this.hostRouter.transitionTo('maintenance.inspection-submissions.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'); } } diff --git a/addon/routes/maintenance/inspection-submissions/index/edit.js b/addon/routes/maintenance/inspection-submissions/index/edit.js index 9e6f0bb2f..424b900eb 100644 --- a/addon/routes/maintenance/inspection-submissions/index/edit.js +++ b/addon/routes/maintenance/inspection-submissions/index/edit.js @@ -6,6 +6,15 @@ export default class MaintenanceInspectionSubmissionsIndexEditRoute extends Rout @service store; @service hostRouter; @service notifications; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops update inspection-submission')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'); + } + } model({ public_id }) { return this.store.findRecord('inspection-submission', public_id); @@ -13,6 +22,6 @@ export default class MaintenanceInspectionSubmissionsIndexEditRoute extends Rout @action error(error) { this.notifications.serverError(error); - return this.hostRouter.transitionTo('maintenance.inspection-submissions.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'); } } diff --git a/addon/routes/maintenance/inspection-submissions/index/new.js b/addon/routes/maintenance/inspection-submissions/index/new.js index f5c57fe09..c27872ab3 100644 --- a/addon/routes/maintenance/inspection-submissions/index/new.js +++ b/addon/routes/maintenance/inspection-submissions/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceInspectionSubmissionsIndexNewRoute extends Route {} +export default class MaintenanceInspectionSubmissionsIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create inspection-submission')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.inspection-submissions.index'); + } + } +} diff --git a/addon/routes/maintenance/maintenances.js b/addon/routes/maintenance/maintenances.js index c38f607a0..67e4ade75 100644 --- a/addon/routes/maintenance/maintenances.js +++ b/addon/routes/maintenance/maintenances.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceMaintenancesRoute extends Route {} +export default class MaintenanceMaintenancesRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list maintenance')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/maintenances/index/details.js b/addon/routes/maintenance/maintenances/index/details.js index 564b2777a..8bdbd46bc 100644 --- a/addon/routes/maintenance/maintenances/index/details.js +++ b/addon/routes/maintenance/maintenances/index/details.js @@ -12,14 +12,14 @@ export default class MaintenanceMaintenancesIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.maintenances.index'); + 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('maintenance.maintenances.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); } } diff --git a/addon/routes/maintenance/maintenances/index/edit.js b/addon/routes/maintenance/maintenances/index/edit.js index 5c81bd470..ec801d142 100644 --- a/addon/routes/maintenance/maintenances/index/edit.js +++ b/addon/routes/maintenance/maintenances/index/edit.js @@ -12,14 +12,14 @@ export default class MaintenanceMaintenancesIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.maintenances.index'); + 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('maintenance.maintenances.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); } } diff --git a/addon/routes/maintenance/maintenances/index/new.js b/addon/routes/maintenance/maintenances/index/new.js index 8bcba94e0..5a5e18f24 100644 --- a/addon/routes/maintenance/maintenances/index/new.js +++ b/addon/routes/maintenance/maintenances/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceMaintenancesIndexNewRoute extends Route {} +export default class MaintenanceMaintenancesIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create maintenance')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.maintenances.index'); + } + } +} diff --git a/addon/routes/maintenance/parts.js b/addon/routes/maintenance/parts.js index 4d1a7132e..5850590f6 100644 --- a/addon/routes/maintenance/parts.js +++ b/addon/routes/maintenance/parts.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenancePartsRoute extends Route {} +export default class MaintenancePartsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list part')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/parts/index/details.js b/addon/routes/maintenance/parts/index/details.js index 9b27d7a72..5a311fb5c 100644 --- a/addon/routes/maintenance/parts/index/details.js +++ b/addon/routes/maintenance/parts/index/details.js @@ -12,14 +12,14 @@ export default class MaintenancePartsIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.parts.index'); + 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('maintenance.parts.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); } } diff --git a/addon/routes/maintenance/parts/index/edit.js b/addon/routes/maintenance/parts/index/edit.js index 0ee51d4a5..18c9c8bbb 100644 --- a/addon/routes/maintenance/parts/index/edit.js +++ b/addon/routes/maintenance/parts/index/edit.js @@ -12,14 +12,14 @@ export default class MaintenancePartsIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.parts.index'); + 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('maintenance.parts.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); } } diff --git a/addon/routes/maintenance/parts/index/new.js b/addon/routes/maintenance/parts/index/new.js index 667dd94d8..022d147ee 100644 --- a/addon/routes/maintenance/parts/index/new.js +++ b/addon/routes/maintenance/parts/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenancePartsIndexNewRoute extends Route {} +export default class MaintenancePartsIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create part')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.parts.index'); + } + } +} diff --git a/addon/routes/maintenance/schedules.js b/addon/routes/maintenance/schedules.js index bcc5af8f2..4de37c319 100644 --- a/addon/routes/maintenance/schedules.js +++ b/addon/routes/maintenance/schedules.js @@ -1,2 +1,16 @@ import Route from '@ember/routing/route'; -export default class MaintenanceSchedulesRoute extends Route {} +import { inject as service } from '@ember/service'; + +export default class MaintenanceSchedulesRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list maintenance-schedule')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/schedules/index/details.js b/addon/routes/maintenance/schedules/index/details.js index a222442e8..45048a427 100644 --- a/addon/routes/maintenance/schedules/index/details.js +++ b/addon/routes/maintenance/schedules/index/details.js @@ -10,13 +10,13 @@ export default class MaintenanceSchedulesIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.schedules.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops view maintenance-schedule')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.schedules.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index'); } } model({ public_id }) { diff --git a/addon/routes/maintenance/schedules/index/edit.js b/addon/routes/maintenance/schedules/index/edit.js index 65d160fdb..44023fc6e 100644 --- a/addon/routes/maintenance/schedules/index/edit.js +++ b/addon/routes/maintenance/schedules/index/edit.js @@ -10,13 +10,13 @@ export default class MaintenanceSchedulesIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.schedules.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index'); } } beforeModel() { if (this.abilities.cannot('fleet-ops update maintenance-schedule')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); - return this.hostRouter.transitionTo('maintenance.schedules.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index'); } } model({ public_id }) { diff --git a/addon/routes/maintenance/schedules/index/new.js b/addon/routes/maintenance/schedules/index/new.js index f0ce7286b..8ed97541c 100644 --- a/addon/routes/maintenance/schedules/index/new.js +++ b/addon/routes/maintenance/schedules/index/new.js @@ -1,2 +1,16 @@ import Route from '@ember/routing/route'; -export default class MaintenanceSchedulesIndexNewRoute extends Route {} +import { inject as service } from '@ember/service'; + +export default class MaintenanceSchedulesIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create maintenance-schedule')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index'); + } + } +} diff --git a/addon/routes/maintenance/work-orders.js b/addon/routes/maintenance/work-orders.js index 13587f950..b9e40da18 100644 --- a/addon/routes/maintenance/work-orders.js +++ b/addon/routes/maintenance/work-orders.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceWorkOrdersRoute extends Route {} +export default class MaintenanceWorkOrdersRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list work-order')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/maintenance/work-orders/index/details.js b/addon/routes/maintenance/work-orders/index/details.js index 6cbee3e24..52738d3f2 100644 --- a/addon/routes/maintenance/work-orders/index/details.js +++ b/addon/routes/maintenance/work-orders/index/details.js @@ -12,14 +12,14 @@ export default class MaintenanceWorkOrdersIndexDetailsRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.work-orders.index'); + 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('maintenance.work-orders.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); } } diff --git a/addon/routes/maintenance/work-orders/index/edit.js b/addon/routes/maintenance/work-orders/index/edit.js index b5a3c8899..1ccfe4bdd 100644 --- a/addon/routes/maintenance/work-orders/index/edit.js +++ b/addon/routes/maintenance/work-orders/index/edit.js @@ -12,14 +12,14 @@ export default class MaintenanceWorkOrdersIndexEditRoute extends Route { @action error(error) { this.notifications.serverError(error); if (typeof error.message === 'string' && error.message.endsWith('not found')) { - return this.hostRouter.transitionTo('maintenance.work-orders.index'); + 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('maintenance.work-orders.index'); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); } } diff --git a/addon/routes/maintenance/work-orders/index/new.js b/addon/routes/maintenance/work-orders/index/new.js index 7757b0a10..fbe3da463 100644 --- a/addon/routes/maintenance/work-orders/index/new.js +++ b/addon/routes/maintenance/work-orders/index/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MaintenanceWorkOrdersIndexNewRoute extends Route {} +export default class MaintenanceWorkOrdersIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create work-order')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.maintenance.work-orders.index'); + } + } +} diff --git a/addon/routes/management/fuel-transactions.js b/addon/routes/management/fuel-transactions.js index 4f0de54b6..c463fb850 100644 --- a/addon/routes/management/fuel-transactions.js +++ b/addon/routes/management/fuel-transactions.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ManagementFuelTransactionsRoute extends Route {} +export default class ManagementFuelTransactionsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list fuel-provider-transaction')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/management/fuel-transactions/index/details.js b/addon/routes/management/fuel-transactions/index/details.js index 05d202d42..b251e2e57 100644 --- a/addon/routes/management/fuel-transactions/index/details.js +++ b/addon/routes/management/fuel-transactions/index/details.js @@ -7,6 +7,15 @@ export default class ManagementFuelTransactionsIndexDetailsRoute extends Route { @service store; @service notifications; @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view fuel-provider-transaction')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.management.fuel-transactions.index'); + } + } @action error(error) { this.notifications.serverError(error); diff --git a/addon/routes/management/index.js b/addon/routes/management/index.js index 0a4dcd62b..63f8af741 100644 --- a/addon/routes/management/index.js +++ b/addon/routes/management/index.js @@ -1,6 +1,19 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class ManagementIndexRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list driver')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } + queryParams = { view: { refreshModel: false }, status: { refreshModel: false }, diff --git a/addon/routes/management/trailers.js b/addon/routes/management/trailers.js new file mode 100644 index 000000000..0b4992833 --- /dev/null +++ b/addon/routes/management/trailers.js @@ -0,0 +1,16 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class ManagementTrailersRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list trailer')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/management/vendors/integrated.js b/addon/routes/management/vendors/integrated.js index 0d90aa4ff..b86b8ac6d 100644 --- a/addon/routes/management/vendors/integrated.js +++ b/addon/routes/management/vendors/integrated.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ManagementVendorsIntegratedRoute extends Route {} +export default class ManagementVendorsIntegratedRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list integrated-vendor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.management.vendors.index'); + } + } +} diff --git a/addon/routes/management/vendors/integrated/details.js b/addon/routes/management/vendors/integrated/details.js index 2c1b8c9b6..e4cb44ae5 100644 --- a/addon/routes/management/vendors/integrated/details.js +++ b/addon/routes/management/vendors/integrated/details.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ManagementVendorsIntegratedDetailsRoute extends Route {} +export default class ManagementVendorsIntegratedDetailsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view integrated-vendor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.management.vendors.index'); + } + } +} diff --git a/addon/routes/management/vendors/integrated/edit.js b/addon/routes/management/vendors/integrated/edit.js index 5167d56f4..5131805c8 100644 --- a/addon/routes/management/vendors/integrated/edit.js +++ b/addon/routes/management/vendors/integrated/edit.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ManagementVendorsIntegratedEditRoute extends Route {} +export default class ManagementVendorsIntegratedEditRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops update integrated-vendor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.management.vendors.index'); + } + } +} diff --git a/addon/routes/management/vendors/integrated/new.js b/addon/routes/management/vendors/integrated/new.js index 1a9ea38c9..1c57c5cc8 100644 --- a/addon/routes/management/vendors/integrated/new.js +++ b/addon/routes/management/vendors/integrated/new.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ManagementVendorsIntegratedNewRoute extends Route {} +export default class ManagementVendorsIntegratedNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create integrated-vendor')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.management.vendors.index'); + } + } +} diff --git a/addon/routes/operations/orders.js b/addon/routes/operations/orders.js index 4d5b88ed7..f4cc46d1e 100644 --- a/addon/routes/operations/orders.js +++ b/addon/routes/operations/orders.js @@ -1,3 +1,42 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class OperationsOrdersRoute extends Route {} +/** + * Orders are the FleetOps landing route (`console.fleet-ops` resolves here), so a user + * without `fleet-ops list order` cannot be sent back to `console.fleet-ops` - that would + * loop. Instead they are forwarded to the first FleetOps area they are permitted to list, + * falling back to the console when there is none. + */ +const FALLBACK_ROUTES = [ + ['fleet-ops list driver', 'console.fleet-ops.management.drivers'], + ['fleet-ops list vehicle', 'console.fleet-ops.management.vehicles'], + ['fleet-ops list trailer', 'console.fleet-ops.management.trailers'], + ['fleet-ops list fleet', 'console.fleet-ops.management.fleets'], + ['fleet-ops list place', 'console.fleet-ops.management.places'], + ['fleet-ops list contact', 'console.fleet-ops.management.contacts'], + ['fleet-ops list vendor', 'console.fleet-ops.management.vendors'], + ['fleet-ops list fuel-report', 'console.fleet-ops.management.fuel-reports'], + ['fleet-ops list issue', 'console.fleet-ops.management.issues'], + ['fleet-ops list work-order', 'console.fleet-ops.maintenance.work-orders'], + ['fleet-ops list device', 'console.fleet-ops.connectivity.devices'], + ['iam list report', 'console.fleet-ops.analytics.reports'], +]; + +export default class OperationsOrdersRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list order')) { + const fallback = FALLBACK_ROUTES.find(([permission]) => this.abilities.can(permission)); + if (fallback) { + return this.hostRouter.transitionTo(fallback[1]); + } + + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console'); + } + } +} diff --git a/addon/routes/operations/routes.js b/addon/routes/operations/routes.js index de8587326..bb6a52e38 100644 --- a/addon/routes/operations/routes.js +++ b/addon/routes/operations/routes.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class OperationsRoutesRoute extends Route {} +export default class OperationsRoutesRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list route')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/operations/routes/index/details.js b/addon/routes/operations/routes/index/details.js index 9ede890da..bf504a0d2 100644 --- a/addon/routes/operations/routes/index/details.js +++ b/addon/routes/operations/routes/index/details.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class OperationsRoutesIndexDetailsRoute extends Route {} +export default class OperationsRoutesIndexDetailsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view route')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.operations.routes.index'); + } + } +} diff --git a/addon/routes/operations/routes/index/new.js b/addon/routes/operations/routes/index/new.js index 3974d9c2c..b2ca429af 100644 --- a/addon/routes/operations/routes/index/new.js +++ b/addon/routes/operations/routes/index/new.js @@ -1,6 +1,19 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class OperationsRoutesIndexNewRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create route')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.operations.routes.index'); + } + } + queryParams = { selectedOrders: { refreshModel: false, diff --git a/addon/routes/operations/service-rates.js b/addon/routes/operations/service-rates.js index 83628deb6..e46ea3081 100644 --- a/addon/routes/operations/service-rates.js +++ b/addon/routes/operations/service-rates.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class OperationsServiceRatesRoute extends Route {} +export default class OperationsServiceRatesRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list service-rate')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/operations/service-rates/index/new.js b/addon/routes/operations/service-rates/index/new.js index 81ceee92f..0659118ce 100644 --- a/addon/routes/operations/service-rates/index/new.js +++ b/addon/routes/operations/service-rates/index/new.js @@ -3,6 +3,17 @@ import { inject as service } from '@ember/service'; export default class OperationsServiceRatesIndexNewRoute extends Route { @service store; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops create service-rate')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops.operations.service-rates.index'); + } + } async setupController(controller) { super.setupController(...arguments); diff --git a/addon/routes/settings/avatars.js b/addon/routes/settings/avatars.js index 22582c4e8..a6cef9606 100644 --- a/addon/routes/settings/avatars.js +++ b/addon/routes/settings/avatars.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class SettingsAvatarsRoute extends Route {} +export default class SettingsAvatarsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list avatar')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/settings/custom-fields.js b/addon/routes/settings/custom-fields.js index 9563a887e..a2bb3fcbc 100644 --- a/addon/routes/settings/custom-fields.js +++ b/addon/routes/settings/custom-fields.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class SettingsCustomFieldsRoute extends Route {} +export default class SettingsCustomFieldsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops list custom-field')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/settings/map.js b/addon/routes/settings/map.js index 51f072976..6b1c6caaa 100644 --- a/addon/routes/settings/map.js +++ b/addon/routes/settings/map.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class SettingsMapRoute extends Route {} +export default class SettingsMapRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view map-settings')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/settings/notifications.js b/addon/routes/settings/notifications.js index 9258c98f6..665c8a638 100644 --- a/addon/routes/settings/notifications.js +++ b/addon/routes/settings/notifications.js @@ -4,6 +4,17 @@ import { hash } from 'rsvp'; export default class SettingsNotificationsRoute extends Route { @service fetch; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view notification-settings')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } model() { return hash({ diff --git a/addon/routes/settings/orchestrator.js b/addon/routes/settings/orchestrator.js index f5109bd46..b6a474dd9 100644 --- a/addon/routes/settings/orchestrator.js +++ b/addon/routes/settings/orchestrator.js @@ -14,7 +14,7 @@ export default class SettingsOrchestratorRoute extends Route { @service hostRouter; beforeModel() { - if (this.abilities.cannot('fleet-ops list order')) { + if (this.abilities.cannot('fleet-ops view routing-settings')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); return this.hostRouter.transitionTo('console.fleet-ops'); } diff --git a/addon/routes/settings/payments/index.js b/addon/routes/settings/payments/index.js index bab0e134c..6925e7aa4 100644 --- a/addon/routes/settings/payments/index.js +++ b/addon/routes/settings/payments/index.js @@ -16,7 +16,7 @@ export default class SettingsPaymentsIndexRoute extends Route { }; beforeModel() { - if (this.abilities.cannot('fleet-ops list purchase-rate')) { + if (this.abilities.cannot('fleet-ops view payments')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); return this.hostRouter.transitionTo('console.fleet-ops'); } diff --git a/addon/routes/settings/routing.js b/addon/routes/settings/routing.js index cd75821fb..bc5a75e59 100644 --- a/addon/routes/settings/routing.js +++ b/addon/routes/settings/routing.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class SettingsRoutingRoute extends Route {} +export default class SettingsRoutingRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view routing-settings')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/settings/scheduling.js b/addon/routes/settings/scheduling.js index c6411b241..c64c6b68b 100644 --- a/addon/routes/settings/scheduling.js +++ b/addon/routes/settings/scheduling.js @@ -1,3 +1,16 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class SettingsSchedulingRoute extends Route {} +export default class SettingsSchedulingRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view scheduling-settings')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } +} diff --git a/addon/routes/settings/telematics.js b/addon/routes/settings/telematics.js new file mode 100644 index 000000000..db07e66d8 --- /dev/null +++ b/addon/routes/settings/telematics.js @@ -0,0 +1,21 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class SettingsTelematicsRoute extends Route { + @service notifications; + @service hostRouter; + @service abilities; + @service intl; + + beforeModel() { + if (this.abilities.cannot('fleet-ops view telematics-settings')) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } + + setupController(controller) { + super.setupController(...arguments); + controller.getSettings.perform(); + } +} diff --git a/addon/routes/virtual.js b/addon/routes/virtual.js index b79b4e462..bbd17220e 100644 --- a/addon/routes/virtual.js +++ b/addon/routes/virtual.js @@ -4,6 +4,10 @@ import { inject as service } from '@ember/service'; export default class VirtualRoute extends Route { @service universe; @service('universe/menu-service') menuService; + @service notifications; + @service hostRouter; + @service abilities; + @service intl; queryParams = { view: { @@ -15,4 +19,11 @@ export default class VirtualRoute extends Route { const view = this.universe.getViewFromTransition(transition); return this.menuService.lookupMenuItem('engine:fleet-ops', slug, view, section); } + + afterModel(menuItem) { + if (menuItem?.permission && this.abilities.cannot(menuItem.permission)) { + this.notifications.warning(this.intl.t('common.unauthorized-access')); + return this.hostRouter.transitionTo('console.fleet-ops'); + } + } } diff --git a/addon/templates/settings/telematics.hbs b/addon/templates/settings/telematics.hbs new file mode 100644 index 000000000..e6bd60696 --- /dev/null +++ b/addon/templates/settings/telematics.hbs @@ -0,0 +1,70 @@ + +