// 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, } }