From 1e93b3d570c97645f33bac83ebf12cf736e60540 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sun, 12 Jul 2026 07:52:21 -0400 Subject: [PATCH] Dissolve System Settings into individual settings pages The monolithic tab page competed with the settings rail as a second navigation system, and its Integrations tab was a dumping ground. Each section is now its own routed rail page (ServiceNow, Zabbix Supplies, Dell Warranty, Collector PC Types, Branding, Floor Map, Printing and Labels, Email/SMTP, Audit, Authentication, Asset Identifiers, Global Search), thin over a shared useSystemSettings composable, grouped logically in the rail with system groups clustered last. Old /settings/system?tab= URLs redirect to the right page. Also fixes the post-login redirect: the auth guard now remembers the intended destination and Login returns there (same-site paths only). Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 14 + docs/COLLECTOR-INTEGRATION.md | 2 +- docs/CONFIG.md | 2 +- docs/UPGRADE.md | 2 +- frontend/src/assets/style.css | 206 ++ frontend/src/composables/systemSettings.js | 346 ++++ frontend/src/router/index.js | 3 +- frontend/src/router/routes/core.js | 93 +- frontend/src/views/Login.vue | 15 +- .../settings/AssetIdentifiersSettings.vue | 58 + frontend/src/views/settings/AuditSettings.vue | 51 + .../views/settings/AuthenticationSettings.vue | 163 ++ .../src/views/settings/BrandingSettings.vue | 142 ++ .../views/settings/DellWarrantySettings.vue | 117 ++ frontend/src/views/settings/EmailSettings.vue | 208 ++ .../src/views/settings/EmployeeDirectory.vue | 2 +- .../src/views/settings/FloorMapSettings.vue | 100 + .../views/settings/GlobalSearchSettings.vue | 45 + .../views/settings/PCTypeMappingSettings.vue | 58 + .../src/views/settings/PrintingSettings.vue | 75 + .../src/views/settings/ServiceNowSettings.vue | 105 + .../src/views/settings/SettingsLayout.vue | 15 +- .../src/views/settings/SystemSettings.vue | 1695 ----------------- .../src/views/settings/ZabbixSettings.vue | 90 + frontend/src/views/settings/settingsNav.js | 37 +- 25 files changed, 1924 insertions(+), 1720 deletions(-) create mode 100644 frontend/src/composables/systemSettings.js create mode 100644 frontend/src/views/settings/AssetIdentifiersSettings.vue create mode 100644 frontend/src/views/settings/AuditSettings.vue create mode 100644 frontend/src/views/settings/AuthenticationSettings.vue create mode 100644 frontend/src/views/settings/BrandingSettings.vue create mode 100644 frontend/src/views/settings/DellWarrantySettings.vue create mode 100644 frontend/src/views/settings/EmailSettings.vue create mode 100644 frontend/src/views/settings/FloorMapSettings.vue create mode 100644 frontend/src/views/settings/GlobalSearchSettings.vue create mode 100644 frontend/src/views/settings/PCTypeMappingSettings.vue create mode 100644 frontend/src/views/settings/PrintingSettings.vue create mode 100644 frontend/src/views/settings/ServiceNowSettings.vue delete mode 100644 frontend/src/views/settings/SystemSettings.vue create mode 100644 frontend/src/views/settings/ZabbixSettings.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index a006808..b4311b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,20 @@ ADR-007 and ADR-002. no-target list with dispositions, a worked idempotent Python importer, and row-count parity checks. +### Changed + +- System Settings is no longer one tabbed page. The inner tab bar is gone and + each section is its own routed settings page reached through the settings + rail: ServiceNow, Zabbix Supplies, Dell Warranty, and Collector PC Types + (the former Integrations dumping ground, now split three-plus ways), plus + Branding, Floor Map, Printing & Labels, Email / SMTP, Authentication, + Audit & Logging, Asset Identifiers, and Global Search. The rail regroups + these under Site & Facility, Integrations, Communication, Search & Identity, + and Access & Security. Shared load/save/upload plumbing moved into a + `useSystemSettings` composable so the pages stay thin. Old bookmarks keep + working: `/settings/system` and every `/settings/system?tab=` redirect + to the matching new page. + ### Fixed - Site & Facility settings page renders booleans as toggles and the diff --git a/docs/COLLECTOR-INTEGRATION.md b/docs/COLLECTOR-INTEGRATION.md index c066ef3..b6fd5cf 100644 --- a/docs/COLLECTOR-INTEGRATION.md +++ b/docs/COLLECTOR-INTEGRATION.md @@ -142,7 +142,7 @@ If you change the payload, change it there and re-check this table. ### pc-type mapping (configurable per site) `pctype` (e.g. `gea-shopfloor-cmm`) maps to a shopdb Computer Type through -`pctypemap_` settings (Settings > System > "Collector PC Type Mapping"). +`pctypemap_` settings (Settings > Collector PC Types). Defaults live in `plugins/computers/pctypemap.py` and are seeded on plugin install; edit per site in the UI. Unmapped pc-types produce a warning, not a failure. diff --git a/docs/CONFIG.md b/docs/CONFIG.md index b24d52a..e1eefe7 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -166,7 +166,7 @@ them under `instance/branding/`. | Key | Default | Notes | |-----|---------|-------| -| `map_blueprint_light` | `/static/images/floorplan-placeholder.svg` | Floor-map blueprint (light theme). Re-upload your own in Settings > Map. | +| `map_blueprint_light` | `/static/images/floorplan-placeholder.svg` | Floor-map blueprint (light theme). Re-upload your own in Settings > Floor Map. | | `map_blueprint_dark` | `/static/images/floorplan-placeholder.svg` | Floor-map blueprint (dark theme). | | `map_width` | `3300` | Blueprint native width in pixels. | | `map_height` | `2550` | Blueprint native height in pixels. | diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md index 0995882..615ad94 100644 --- a/docs/UPGRADE.md +++ b/docs/UPGRADE.md @@ -97,7 +97,7 @@ removes those bundled PNGs and ships a generic placeholder SVG instead. If your instance's `map_blueprint_light` / `map_blueprint_dark` Settings still point at `/static/images/sitemap2025-*`, the map will 404 those images after the -upgrade. Re-upload your own floor plan in **Settings > Map**. Uploaded floor +upgrade. Re-upload your own floor plan in **Settings > Floor Map**. Uploaded floor plans are stored under `instance/` and survive upgrades, so a site that already uploaded its own plan is unaffected. Only instances still using the old bundled default need to act. diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css index 94c4917..cb67a49 100644 --- a/frontend/src/assets/style.css +++ b/frontend/src/assets/style.css @@ -1083,6 +1083,212 @@ td.actions { border-bottom: 1px solid var(--border); } +/* Settings form controls - shared by the individual settings pages + (branding, printing, email, integrations, identifiers, search, map...). */ +.setting-group { + border-top: 1px solid var(--border); + padding-top: 1rem; +} +.setting-group:first-of-type { + border-top: none; + padding-top: 0; +} +.setting-group h3 { + margin: 0 0 0.5rem 0; + font-size: 1rem; + color: var(--text); +} +.setting-description { + color: var(--text-light); + font-size: 0.9rem; + margin: 0 0 1rem 0; + line-height: 1.5; +} +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; + margin-bottom: 1rem; +} +.setting-row { + margin-bottom: 1rem; +} +.setting-row.full-width { + grid-column: 1 / -1; +} +.setting-row label { + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.setting-row label span { + color: var(--text); + font-size: 0.9rem; +} +.setting-row input, +.setting-row select { + padding: 0.5rem; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--bg); + color: var(--text); + font-size: 1rem; + max-width: 400px; +} +.setting-row input[type="number"] { + max-width: 120px; +} +.setting-row input:focus, +.setting-row select:focus { + outline: none; + border-color: var(--primary); +} +.input-hint { + color: var(--text-light); + font-size: 0.8rem; +} +.toggle-label { + flex-direction: row !important; + align-items: center; + justify-content: space-between; + max-width: 400px; +} +.toggle-hint { + display: block; + margin-top: -0.5rem; + margin-left: 0; +} +.toggle-btn { + position: relative; + width: 50px; + height: 26px; + border-radius: 13px; + border: none; + background: var(--secondary); + cursor: pointer; + transition: background 0.2s; +} +.toggle-btn.active { + background: var(--success); +} +.toggle-slider { + position: absolute; + top: 3px; + left: 3px; + width: 20px; + height: 20px; + border-radius: 50%; + background: white; + transition: transform 0.2s; +} +.toggle-btn.active .toggle-slider { + transform: translateX(24px); +} +.status-indicator { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem; + background: var(--bg); + border-radius: 4px; + font-size: 0.9rem; + color: var(--text-light); + max-width: 400px; + margin-bottom: 1rem; +} +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} +.status-dot.inactive { background: var(--secondary); } +.status-dot.warning { background: var(--warning); } +.status-dot.pending { background: var(--primary); } +.status-dot.success { background: var(--success); } +.test-btn { + padding: 0.5rem 1rem; + background: var(--primary); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; +} +.test-btn:hover:not(:disabled) { + background: var(--primary-dark); +} +.test-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.view-logs-link { + display: inline-block; + margin-top: 0.5rem; + color: var(--link); + text-decoration: none; + font-size: 0.9rem; +} +.view-logs-link:hover { + text-decoration: underline; +} +.identifier-matrix { + width: 100%; + border-collapse: collapse; +} +.identifier-matrix th, +.identifier-matrix td { + padding: 0.6rem 0.75rem; + text-align: center; + border-bottom: 1px solid var(--border); +} +.identifier-matrix th:first-child, +.identifier-matrix td.identifier-name { + text-align: left; +} +.identifier-matrix th { + color: var(--text-light); + font-weight: 600; + font-size: 0.9rem; +} +.identifier-matrix .identifier-name { + color: var(--text); +} +.color-input-row { + display: flex; + align-items: center; + gap: 0.75rem; +} +.color-input-row input[type="color"] { + width: 48px; + height: 34px; + padding: 2px; + cursor: pointer; +} +.color-input-row input[type="text"] { + max-width: 160px; +} +.map-upload-row { + display: flex; + align-items: center; + gap: 0.75rem; + margin-top: 0.4rem; +} +.map-thumb { + height: 40px; + border: 1px solid var(--border); + border-radius: 4px; + background: #fff; +} +.map-thumb-dark { background: #222; } +.settings-success { + margin-top: 1rem; + padding: 0.75rem; + background: var(--success); + color: white; + border-radius: 4px; +} + /* Info List */ .info-list { display: flex; diff --git a/frontend/src/composables/systemSettings.js b/frontend/src/composables/systemSettings.js new file mode 100644 index 0000000..7c21e94 --- /dev/null +++ b/frontend/src/composables/systemSettings.js @@ -0,0 +1,346 @@ +// Shared plumbing for the individual system-settings pages. +// Each settings page calls useSystemSettings() in its own setup, so state is +// per-page (no cross-page leak). The composable owns the load-all/save/toggle/ +// upload machinery plus the static display catalogs, keeping the pages thin. +// Per-page connection-status computeds (zabbix/dell/smtp/saml) stay in the pages. +import { ref, reactive } from 'vue' +import { settingsApi, computersApi } from '../api' +import { setIdentifierFlag } from './identifierSettings' +import { apiError } from '../utils/apiError' + +// Branding logo widgets. kind maps to the backend endpoint; key is the setting +// the resulting URL is stored under. +export const brandingLogos = [ + { kind: 'site', key: 'site_logo', label: 'Site logo', accept: 'image/*', + placeholder: '/ge-aerospace-logo.svg', + hint: 'Shown in the app header and login. Upload an image or type a path/URL.' }, + { kind: 'qr', key: 'qr_logo', label: 'QR overlay logo', accept: 'image/*', + placeholder: '/ge-monogram.svg', + hint: 'Logo overlaid on printed QR codes. Leave blank for no overlay.' }, + { kind: 'badge', key: 'badge_logo', label: 'Machine badge logo', accept: 'image/*', + placeholder: '/ge-aerospace-logo.svg', + hint: 'Logo printed on machine badges. Upload an image or type a path/URL.' }, + { kind: 'favicon', key: 'site_favicon', label: 'Favicon', accept: 'image/*,.ico', + placeholder: '(blank = shipped /favicon.svg)', + hint: 'Browser-tab icon. Leave blank to use the shipped favicon.' }, +] + +// Asset identifier matrix: identifier x asset type. Keys follow +// identifier___enabled. Missing = enabled (default on). +export const identifierRows = [ + { name: 'gaugelabreference', label: 'Gauge Lab Reference' }, + { name: 'maintenancereference', label: 'Maintenance Reference' }, + { name: 'fqdn', label: 'FQDN / Hostname' } +] +export const assetTypeCols = [ + { key: 'machine', label: 'Machine' }, + { key: 'computer', label: 'PC' }, + { key: 'printer', label: 'Printer' }, + { key: 'network_device', label: 'Network' } +] + +// Global-search domain toggles: keys follow search__enabled. +// Missing = enabled (default on). +export const searchDomains = [ + { key: 'application', label: 'Applications' }, + { key: 'knowledgebase', label: 'Knowledge Base' }, + { key: 'employee', label: 'Employees' }, + { key: 'machine', label: 'Machines' }, + { key: 'computer', label: 'PCs' }, + { key: 'printer', label: 'Printers' }, + { key: 'network_device', label: 'Network Devices' }, + { key: 'notification', label: 'Notifications' }, + { key: 'subnet', label: 'Subnets' } +] + +export function useSystemSettings() { + // Every settings key across all pages. A page binds only its own subset. + const settings = reactive({ + // Zabbix + zabbix_enabled: false, + zabbix_url: '', + zabbix_token: '', + // Dell warranty + warranty_dell_enabled: false, + warranty_dell_clientid: '', + warranty_dell_clientsecret: '', + warranty_dell_tokenurl: '', + warranty_dell_apiurl: '', + // ServiceNow + servicenow_enabled: true, + servicenow_search_url: '', + servicenow_ticket_prefixes: '', + servicenow_incident_url: '', + servicenow_change_url: '', + // Branding + site_logo: '', + qr_logo: '', + badge_logo: '', + site_favicon: '', + brand_primary_color: '', + brand_primary_dark_color: '', + brand_accent_color: '', + brand_sidebar_color: '', + // Printing and labels + qr_target_printer: '', + qr_target_usb: '', + usb_label_style: 'barcode', + // Email + smtp_enabled: false, + smtp_host: '', + smtp_port: 587, + smtp_username: '', + smtp_password: '', + smtp_use_tls: true, + smtp_from_address: '', + smtp_from_name: 'ShopDB', + alert_recipients: '', + // Audit + audit_retention_days: 90, + // Floor map blueprint (per-facility) + map_blueprint_light: '', + map_blueprint_dark: '', + map_width: 3300, + map_height: 2550, + // SAML + saml_enabled: false, + saml_idp_metadata_url: '', + saml_entity_id: '', + saml_acs_url: '', + saml_allow_local_login: true, + saml_auto_create_users: true, + saml_admin_group: '' + }) + + const identifierMatrix = reactive({}) + const searchMatrix = reactive({}) + const pcTypeMappings = ref([]) // [{ pxetype, computertype }] + const computerTypes = ref([]) // ComputerType names for the dropdown + + const loading = ref(true) + const saving = ref(false) + const mapUploading = ref(false) + const brandingUploading = ref(false) + const error = ref('') + const success = ref('') + + function flashSaved() { + success.value = 'Setting saved' + setTimeout(() => { success.value = '' }, 2000) + } + + function identifierKey(name, assettype) { + return `identifier_${name}_${assettype}_enabled` + } + + function matrixValue(name, assettype) { + const key = identifierKey(name, assettype) + return key in identifierMatrix ? identifierMatrix[key] : true + } + + function searchValue(domainKey) { + const key = `search_${domainKey}_enabled` + return key in searchMatrix ? searchMatrix[key] : true + } + + // Load all settings and fan them into settings / matrices / pctype rows. + async function loadSettings() { + try { + loading.value = true + const { data } = await settingsApi.list() + + const pctypeRows = [] + for (const setting of data.data) { + if (setting.key in settings) { + settings[setting.key] = setting.value + } else if (/^identifier_.+_(machine|computer|printer|network_device)_enabled$/.test(setting.key)) { + identifierMatrix[setting.key] = setting.value !== false + } else if (/^search_.+_enabled$/.test(setting.key)) { + searchMatrix[setting.key] = setting.value !== false + } else if (setting.key.startsWith('pctypemap_')) { + pctypeRows.push({ + pxetype: setting.key.slice('pctypemap_'.length), + computertype: setting.value + }) + } + } + pcTypeMappings.value = pctypeRows.sort((a, b) => a.pxetype.localeCompare(b.pxetype)) + } catch (e) { + error.value = 'Failed to load settings' + console.error(e) + } finally { + loading.value = false + } + } + + // Computer type options for the pctype mapping dropdown (own request). + async function loadComputerTypes() { + try { + const typesResponse = await computersApi.types.list({ perpage: 100 }) + computerTypes.value = (typesResponse.data.data || []).map(t => t.computertype) + } catch (typesError) { + console.error('Failed to load computer types', typesError) + } + } + + async function saveSetting(key, value) { + try { + saving.value = true + error.value = '' + success.value = '' + await settingsApi.update(key, value) + settings[key] = value + flashSaved() + } catch (e) { + error.value = apiError(e, 'Failed to save setting') + console.error(e) + } finally { + saving.value = false + } + } + + async function toggleSetting(key) { + await saveSetting(key, !settings[key]) + } + + // Toggle a per-type identifier flag. The key may not be seeded on older + // installs, so create it when the update returns 404. + async function toggleIdentifier(name, assettype) { + const key = identifierKey(name, assettype) + const newValue = !matrixValue(name, assettype) + try { + saving.value = true + error.value = '' + success.value = '' + try { + await settingsApi.update(key, newValue) + } catch (e) { + if (e.response?.status === 404) { + await settingsApi.create({ + key, + value: newValue, + valuetype: 'boolean', + category: 'identifiers', + description: `Show the ${name} identifier on ${assettype} assets` + }) + } else { + throw e + } + } + identifierMatrix[key] = newValue + // Push into the shared composable so open asset views react without a + // full page reload (the composable otherwise fetches only once). + setIdentifierFlag(name, assettype, newValue) + flashSaved() + } catch (e) { + error.value = apiError(e, 'Failed to save setting') + console.error(e) + } finally { + saving.value = false + } + } + + async function toggleSearchDomain(domainKey) { + const key = `search_${domainKey}_enabled` + const newValue = !searchValue(domainKey) + const label = searchDomains.find(d => d.key === domainKey)?.label || domainKey + try { + saving.value = true + error.value = '' + success.value = '' + try { + await settingsApi.update(key, newValue) + } catch (e) { + if (e.response?.status === 404) { + await settingsApi.create({ + key, + value: newValue, + valuetype: 'boolean', + category: 'search', + description: `Include ${label} in global search results` + }) + } else { + throw e + } + } + searchMatrix[key] = newValue + flashSaved() + } catch (e) { + error.value = apiError(e, 'Failed to save setting') + console.error(e) + } finally { + saving.value = false + } + } + + async function changePcTypeMapping(pxetype, computertype) { + const key = `pctypemap_${pxetype}` + try { + saving.value = true + error.value = '' + success.value = '' + await settingsApi.update(key, computertype) + const row = pcTypeMappings.value.find(r => r.pxetype === pxetype) + if (row) row.computertype = computertype + flashSaved() + } catch (e) { + error.value = apiError(e, 'Failed to save setting') + console.error(e) + } finally { + saving.value = false + } + } + + async function uploadBlueprint(theme, event) { + const file = event.target.files[0] + if (!file) return + mapUploading.value = true + error.value = '' + success.value = '' + try { + const { data } = await settingsApi.uploadMapBlueprint(theme, file) + const url = data?.data?.value + if (theme === 'light') settings.map_blueprint_light = url + else settings.map_blueprint_dark = url + success.value = 'Blueprint uploaded' + setTimeout(() => { success.value = '' }, 2000) + } catch (e) { + error.value = apiError(e, 'Upload failed') + } finally { + mapUploading.value = false + event.target.value = '' + } + } + + async function uploadLogo(kind, key, event) { + const file = event.target.files[0] + if (!file) return + brandingUploading.value = true + error.value = '' + success.value = '' + try { + const { data } = await settingsApi.uploadBrandingLogo(kind, file) + const url = data?.data?.value + if (url) settings[key] = url + success.value = 'Logo uploaded' + setTimeout(() => { success.value = '' }, 2000) + } catch (e) { + error.value = apiError(e, 'Upload failed') + } finally { + brandingUploading.value = false + event.target.value = '' + } + } + + return { + settings, + identifierMatrix, searchMatrix, pcTypeMappings, computerTypes, + loading, saving, mapUploading, brandingUploading, error, success, + loadSettings, loadComputerTypes, + saveSetting, toggleSetting, + identifierKey, matrixValue, toggleIdentifier, + searchValue, toggleSearchDomain, + changePcTypeMapping, + uploadBlueprint, uploadLogo, + } +} diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index af2d639..cf21db8 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -105,7 +105,8 @@ router.beforeEach(async (to, from, next) => { const authStore = useAuthStore() if (to.meta.requiresAuth && !authStore.isAuthenticated) { - return next('/login') + // Remember where they were headed so login can send them back there. + return next({ path: '/login', query: { redirect: to.fullPath } }) } if (to.meta.requiresAdmin && !authStore.isAdmin) { return next('/') diff --git a/frontend/src/router/routes/core.js b/frontend/src/router/routes/core.js index d091bb5..ea35ef3 100644 --- a/frontend/src/router/routes/core.js +++ b/frontend/src/router/routes/core.js @@ -161,10 +161,101 @@ export default [ component: () => import('../../views/settings/EmployeeDirectory.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, + // The old monolithic System Settings tab page is split into one routed page + // per section (below). This redirect keeps old bookmarks working: bare + // /settings/system and every /settings/system?tab= land on the right page. { path: 'settings/system', name: 'system-settings', - component: () => import('../../views/settings/SystemSettings.vue'), + redirect: to => { + const tabRedirects = { + integrations: '/settings/servicenow', + branding: '/settings/branding', + printing: '/settings/printing', + email: '/settings/email', + audit: '/settings/audit', + auth: '/settings/authentication', + identifiers: '/settings/assetidentifiers', + search: '/settings/globalsearch', + map: '/settings/floormap', + pctype: '/settings/pctypemapping' + } + // vue-router carries the original ?tab= query onto the target; harmless + // since the new pages ignore it, and the path is what routes the view. + return tabRedirects[to.query.tab] || '/settings/servicenow' + }, + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/servicenow', + name: 'servicenow-settings', + component: () => import('../../views/settings/ServiceNowSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/zabbix', + name: 'zabbix-settings', + component: () => import('../../views/settings/ZabbixSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/dellwarranty', + name: 'dell-warranty-settings', + component: () => import('../../views/settings/DellWarrantySettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/pctypemapping', + name: 'pctype-mapping-settings', + component: () => import('../../views/settings/PCTypeMappingSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/branding', + name: 'branding-settings', + component: () => import('../../views/settings/BrandingSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/printing', + name: 'printing-settings', + component: () => import('../../views/settings/PrintingSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/email', + name: 'email-settings', + component: () => import('../../views/settings/EmailSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/audit', + name: 'audit-settings', + component: () => import('../../views/settings/AuditSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/authentication', + name: 'authentication-settings', + component: () => import('../../views/settings/AuthenticationSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/assetidentifiers', + name: 'asset-identifiers-settings', + component: () => import('../../views/settings/AssetIdentifiersSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/globalsearch', + name: 'global-search-settings', + component: () => import('../../views/settings/GlobalSearchSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: 'settings/floormap', + name: 'floor-map-settings', + component: () => import('../../views/settings/FloorMapSettings.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, { diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index 57cd4a4..751d45b 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -48,14 +48,25 @@ diff --git a/frontend/src/views/settings/AuditSettings.vue b/frontend/src/views/settings/AuditSettings.vue new file mode 100644 index 0000000..7831da0 --- /dev/null +++ b/frontend/src/views/settings/AuditSettings.vue @@ -0,0 +1,51 @@ + + + diff --git a/frontend/src/views/settings/AuthenticationSettings.vue b/frontend/src/views/settings/AuthenticationSettings.vue new file mode 100644 index 0000000..4bc673a --- /dev/null +++ b/frontend/src/views/settings/AuthenticationSettings.vue @@ -0,0 +1,163 @@ + + + diff --git a/frontend/src/views/settings/BrandingSettings.vue b/frontend/src/views/settings/BrandingSettings.vue new file mode 100644 index 0000000..bf455bc --- /dev/null +++ b/frontend/src/views/settings/BrandingSettings.vue @@ -0,0 +1,142 @@ + + + diff --git a/frontend/src/views/settings/DellWarrantySettings.vue b/frontend/src/views/settings/DellWarrantySettings.vue new file mode 100644 index 0000000..8c3a0da --- /dev/null +++ b/frontend/src/views/settings/DellWarrantySettings.vue @@ -0,0 +1,117 @@ + + + diff --git a/frontend/src/views/settings/EmailSettings.vue b/frontend/src/views/settings/EmailSettings.vue new file mode 100644 index 0000000..bcbd1b0 --- /dev/null +++ b/frontend/src/views/settings/EmailSettings.vue @@ -0,0 +1,208 @@ + + + diff --git a/frontend/src/views/settings/EmployeeDirectory.vue b/frontend/src/views/settings/EmployeeDirectory.vue index 612442e..0d9642b 100644 --- a/frontend/src/views/settings/EmployeeDirectory.vue +++ b/frontend/src/views/settings/EmployeeDirectory.vue @@ -13,7 +13,7 @@ The directory is in external mode - people come from a separate HR database, so there is nothing to manage here. To manage people in-app, set employee_directory_mode to - selfhosted under System Settings, then reload. + selfhosted under Site & Facility settings, then reload.

diff --git a/frontend/src/views/settings/FloorMapSettings.vue b/frontend/src/views/settings/FloorMapSettings.vue new file mode 100644 index 0000000..624918f --- /dev/null +++ b/frontend/src/views/settings/FloorMapSettings.vue @@ -0,0 +1,100 @@ + + + diff --git a/frontend/src/views/settings/GlobalSearchSettings.vue b/frontend/src/views/settings/GlobalSearchSettings.vue new file mode 100644 index 0000000..659f198 --- /dev/null +++ b/frontend/src/views/settings/GlobalSearchSettings.vue @@ -0,0 +1,45 @@ + + + diff --git a/frontend/src/views/settings/PCTypeMappingSettings.vue b/frontend/src/views/settings/PCTypeMappingSettings.vue new file mode 100644 index 0000000..e732aa6 --- /dev/null +++ b/frontend/src/views/settings/PCTypeMappingSettings.vue @@ -0,0 +1,58 @@ + + + diff --git a/frontend/src/views/settings/PrintingSettings.vue b/frontend/src/views/settings/PrintingSettings.vue new file mode 100644 index 0000000..83742ee --- /dev/null +++ b/frontend/src/views/settings/PrintingSettings.vue @@ -0,0 +1,75 @@ + + + diff --git a/frontend/src/views/settings/ServiceNowSettings.vue b/frontend/src/views/settings/ServiceNowSettings.vue new file mode 100644 index 0000000..bfdb8c3 --- /dev/null +++ b/frontend/src/views/settings/ServiceNowSettings.vue @@ -0,0 +1,105 @@ + + + diff --git a/frontend/src/views/settings/SettingsLayout.vue b/frontend/src/views/settings/SettingsLayout.vue index 02e4056..ee2fc20 100644 --- a/frontend/src/views/settings/SettingsLayout.vue +++ b/frontend/src/views/settings/SettingsLayout.vue @@ -59,20 +59,11 @@ const visibleGroups = computed(() => { .filter(g => g.cards.length) }) -// A rail link is active when the current path matches its base path -// (ignoring query, so /settings/system?tab=map and /settings/system stay distinct -// only by their own comparison below). +// A rail link is active when the current path matches its base path. +// Every settings section is now its own page, so a plain path compare suffices. function isActive(to) { const base = to.split('?')[0] - const query = to.includes('?') ? to.split('?')[1] : '' - if (route.path !== base) return false - // Floor Map shares /settings/system with System Settings; disambiguate by tab. - if (base === '/settings/system') { - const wantMap = query.includes('tab=map') - const onMap = route.query.tab === 'map' - return wantMap === onMap - } - return true + return route.path === base } diff --git a/frontend/src/views/settings/SystemSettings.vue b/frontend/src/views/settings/SystemSettings.vue deleted file mode 100644 index ec52a5f..0000000 --- a/frontend/src/views/settings/SystemSettings.vue +++ /dev/null @@ -1,1695 +0,0 @@ - - - - - diff --git a/frontend/src/views/settings/ZabbixSettings.vue b/frontend/src/views/settings/ZabbixSettings.vue new file mode 100644 index 0000000..4289e44 --- /dev/null +++ b/frontend/src/views/settings/ZabbixSettings.vue @@ -0,0 +1,90 @@ + + + diff --git a/frontend/src/views/settings/settingsNav.js b/frontend/src/views/settings/settingsNav.js index 5939cd7..7fd4a4c 100644 --- a/frontend/src/views/settings/settingsNav.js +++ b/frontend/src/views/settings/settingsNav.js @@ -1,15 +1,18 @@ // Shared settings navigation catalog. // Used by SettingsLayout (left rail) and SettingsIndex (landing overview) so the // grouping lives in one place. -import { Factory, MapPin, Tag, Package, Droplets, Monitor, MonitorSmartphone, Laptop, Cog, Building, Globe, Link, Settings, FileText, Users, Puzzle, Bell, Network, Home, Wrench, Printer, Router, Palette, SlidersHorizontal, Contact } from 'lucide-vue-next' +// Order: site identity + the reference-data catalogs users touch daily come +// first, then the platform/system groups (integrations, communication, search, +// plugins, access) cluster together at the end. +import { Factory, MapPin, Tag, Package, Droplets, Monitor, MonitorSmartphone, Laptop, Cog, Building, Globe, Link, History, FileText, Users, Puzzle, Bell, Network, Home, Wrench, Printer, Router, Palette, SlidersHorizontal, Contact, Mail, ShieldCheck, KeyRound, Fingerprint, Search } from 'lucide-vue-next' export const settingsGroups = [ { title: 'Site & Facility', cards: [ { to: '/settings/site', icon: Home, title: 'Site & Facility', description: 'Site URL/FQDN, facility name, PC access domain, employee-ID pattern, printer hostname template' }, - { to: '/settings/system?tab=map', icon: MapPin, title: 'Floor Map', description: 'Facility floor-plan blueprint and dimensions' }, - { to: '/settings/system?tab=branding', icon: Palette, title: 'Branding', description: 'Site, QR, and badge logos, favicon, and primary brand color' }, + { to: '/settings/floormap', icon: MapPin, title: 'Floor Map', description: 'Facility floor-plan blueprint image and pixel dimensions (width, height) for the site map' }, + { to: '/settings/branding', icon: Palette, title: 'Branding', description: 'Site, QR, and badge logos, favicon, and brand primary, accent, sidebar, and theme colors' }, ], }, { @@ -74,17 +77,41 @@ export const settingsGroups = [ { to: '/settings/notificationtypes', icon: Bell, title: 'Notification Types', description: 'Manage notification types, display styles, colors, and auto-expiry' }, ], }, + { + title: 'Integrations', + cards: [ + { to: '/settings/servicenow', icon: Link, title: 'ServiceNow', description: 'ServiceNow ticket links (incident, change) prefixes and global-search redirect' }, + { to: '/settings/zabbix', icon: Droplets, title: 'Zabbix Supplies', description: 'Zabbix API for real-time printer toner and supply monitoring' }, + { to: '/settings/dellwarranty', icon: ShieldCheck, title: 'Dell Warranty', description: 'Dell TechDirect warranty API lookup by service tag' }, + { to: '/settings/pctypemapping', icon: Laptop, title: 'Collector PC Types', description: 'Map collector enrollment imaging pc-type (shopfloor) to a Computer Type' }, + ], + }, + { + title: 'Communication', + cards: [ + { to: '/settings/email', icon: Mail, title: 'Email / SMTP', description: 'SMTP mail host for email notifications, alerts, TLS, from address, and recipients' }, + ], + }, + { + title: 'Search & Identity', + cards: [ + { to: '/settings/globalsearch', icon: Search, title: 'Global Search', description: 'Choose which content types (domains) appear in global search results' }, + { to: '/settings/assetidentifiers', icon: Fingerprint, title: 'Asset Identifiers', description: 'Enable optional asset identifiers per type: gauge lab, maintenance, FQDN hostname' }, + { to: '/settings/printing', icon: Printer, title: 'Printing & Labels', description: 'Printed QR code and USB label targets, barcode style, and URL templates' }, + ], + }, { title: 'System', cards: [ - { to: '/settings/system', icon: Settings, title: 'System Settings', description: 'Integrations (ServiceNow, Zabbix), branding, identifiers, search, and PC-type mapping' }, { to: '/settings/plugins', icon: Puzzle, title: 'Plugins', description: 'Enable or disable installed plugins' }, ], }, { - title: 'Access & Audit', + title: 'Access & Security', cards: [ + { to: '/settings/authentication', icon: KeyRound, title: 'Authentication', description: 'SAML single sign-on (SSO) with your IdP, local login, and auto-create users' }, { to: '/settings/users', icon: Users, title: 'Users & Roles', description: 'Manage user accounts and permissions' }, + { to: '/settings/audit', icon: History, title: 'Audit & Logging', description: 'Audit log retention period and history purge policy' }, { to: '/settings/auditlogs', icon: FileText, title: 'Audit Logs', description: 'View system activity and change history' }, ], },