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 <noreply@anthropic.com>
This commit is contained in:
@@ -48,14 +48,25 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { setupApi } from '../api'
|
||||
import { getSiteLogo } from '../utils/siteSettings'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Where to land after login: the guard-supplied ?redirect target, else the
|
||||
// dashboard. Only accept same-site absolute paths to avoid open redirects.
|
||||
function postLoginTarget() {
|
||||
const redirect = route.query.redirect
|
||||
if (typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//')) {
|
||||
return redirect
|
||||
}
|
||||
return '/'
|
||||
}
|
||||
|
||||
const siteLogo = ref('/ge-aerospace-logo.svg')
|
||||
const mode = ref('login')
|
||||
const username = ref('')
|
||||
@@ -79,7 +90,7 @@ async function handleLogin() {
|
||||
loading.value = true
|
||||
const result = await authStore.login(username.value, password.value)
|
||||
loading.value = false
|
||||
if (result.success) router.push('/')
|
||||
if (result.success) router.push(postLoginTarget())
|
||||
else error.value = result.message
|
||||
}
|
||||
|
||||
|
||||
58
frontend/src/views/settings/AssetIdentifiersSettings.vue
Normal file
58
frontend/src/views/settings/AssetIdentifiersSettings.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Asset Identifiers</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<p class="setting-description">
|
||||
Enable or disable optional asset identifiers per asset type. When disabled
|
||||
for a type, the identifier is hidden from that type's forms and detail
|
||||
pages across the system.
|
||||
</p>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="identifier-matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Identifier</th>
|
||||
<th v-for="col in assetTypeCols" :key="col.key">{{ col.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in identifierRows" :key="row.name">
|
||||
<td class="identifier-name">{{ row.label }}</td>
|
||||
<td v-for="col in assetTypeCols" :key="col.key">
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: matrixValue(row.name, col.key) }"
|
||||
@click="toggleIdentifier(row.name, col.key)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useSystemSettings, identifierRows, assetTypeCols } from '../../composables/systemSettings'
|
||||
|
||||
const {
|
||||
saving, error, success,
|
||||
loadSettings, matrixValue, toggleIdentifier,
|
||||
} = useSystemSettings()
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
51
frontend/src/views/settings/AuditSettings.vue
Normal file
51
frontend/src/views/settings/AuditSettings.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Audit & Logging</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<h3>Audit Log Retention</h3>
|
||||
<p class="setting-description">
|
||||
Configure how long audit logs are retained. Older logs will be automatically purged.
|
||||
Set to 0 to keep logs indefinitely.
|
||||
</p>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Retention Period (days)</span>
|
||||
<input
|
||||
type="number"
|
||||
v-model="settings.audit_retention_days"
|
||||
min="0"
|
||||
placeholder="90"
|
||||
@blur="saveSetting('audit_retention_days', settings.audit_retention_days)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">0 = keep forever, recommended: 90 days</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<router-link to="/settings/auditlogs" class="view-logs-link">
|
||||
View Audit Logs →
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useSystemSettings } from '../../composables/systemSettings'
|
||||
|
||||
const {
|
||||
settings, saving, error, success,
|
||||
loadSettings, saveSetting,
|
||||
} = useSystemSettings()
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
163
frontend/src/views/settings/AuthenticationSettings.vue
Normal file
163
frontend/src/views/settings/AuthenticationSettings.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Authentication</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<h3>SAML Single Sign-On</h3>
|
||||
<p class="setting-description">
|
||||
Enable SAML SSO for enterprise authentication. Users can sign in using your organization's
|
||||
identity provider (Azure AD, Okta, etc.). Local login can remain enabled as a fallback.
|
||||
</p>
|
||||
|
||||
<div class="setting-row">
|
||||
<label class="toggle-label">
|
||||
<span>Enable SAML SSO</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: settings.saml_enabled }"
|
||||
@click="toggleSetting('saml_enabled')"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<template v-if="settings.saml_enabled">
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Identity Provider Metadata URL</span>
|
||||
<input
|
||||
type="url"
|
||||
v-model="settings.saml_idp_metadata_url"
|
||||
placeholder="https://login.microsoftonline.com/.../federationmetadata.xml"
|
||||
@blur="saveSetting('saml_idp_metadata_url', settings.saml_idp_metadata_url)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Service Provider Entity ID</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.saml_entity_id"
|
||||
placeholder="https://shopdb.example.com"
|
||||
@blur="saveSetting('saml_entity_id', settings.saml_entity_id)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">Unique identifier for this application</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Assertion Consumer Service URL</span>
|
||||
<input
|
||||
type="url"
|
||||
v-model="settings.saml_acs_url"
|
||||
placeholder="https://shopdb.example.com/api/auth/saml/acs"
|
||||
@blur="saveSetting('saml_acs_url', settings.saml_acs_url)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">URL where SAML responses are sent</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Admin Group Name</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.saml_admin_group"
|
||||
placeholder="ShopDB-Admins"
|
||||
@blur="saveSetting('saml_admin_group', settings.saml_admin_group)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">SAML group that grants admin role (leave empty to manage manually)</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label class="toggle-label">
|
||||
<span>Allow Local Login</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: settings.saml_allow_local_login }"
|
||||
@click="toggleSetting('saml_allow_local_login')"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</label>
|
||||
<small class="input-hint toggle-hint">Keep enabled for admin fallback access</small>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label class="toggle-label">
|
||||
<span>Auto-Create Users</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: settings.saml_auto_create_users }"
|
||||
@click="toggleSetting('saml_auto_create_users')"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</label>
|
||||
<small class="input-hint toggle-hint">Automatically create user accounts on first SAML login</small>
|
||||
</div>
|
||||
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot" :class="samlStatus"></span>
|
||||
<span>{{ samlMessage }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<h3>User Management</h3>
|
||||
<p class="setting-description">
|
||||
Manage local user accounts and roles. Local accounts work alongside SAML when both are enabled.
|
||||
</p>
|
||||
|
||||
<router-link to="/settings/users" class="view-logs-link">
|
||||
Manage Users →
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { useSystemSettings } from '../../composables/systemSettings'
|
||||
|
||||
const {
|
||||
settings, saving, error, success,
|
||||
loadSettings, saveSetting, toggleSetting,
|
||||
} = useSystemSettings()
|
||||
|
||||
// Per-page connection status (stays with the page, not the composable).
|
||||
const samlStatus = computed(() => {
|
||||
if (!settings.saml_enabled) return 'inactive'
|
||||
if (!settings.saml_idp_metadata_url || !settings.saml_entity_id) return 'warning'
|
||||
return 'pending'
|
||||
})
|
||||
|
||||
const samlMessage = computed(() => {
|
||||
if (!settings.saml_enabled) return 'Disabled - using local authentication only'
|
||||
if (!settings.saml_idp_metadata_url) return 'IdP metadata URL not configured'
|
||||
if (!settings.saml_entity_id) return 'Entity ID not configured'
|
||||
return 'Configured - SAML login available'
|
||||
})
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
142
frontend/src/views/settings/BrandingSettings.vue
Normal file
142
frontend/src/views/settings/BrandingSettings.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Branding</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<p class="setting-description">
|
||||
Replace the shipped GE logos with your own site branding. Each logo can
|
||||
be uploaded, or set to a path/URL directly. Leave blank to use the
|
||||
bundled default.
|
||||
</p>
|
||||
|
||||
<div class="setting-row" v-for="logo in brandingLogos" :key="logo.kind">
|
||||
<label>
|
||||
<span>{{ logo.label }}</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings[logo.key]"
|
||||
:placeholder="logo.placeholder"
|
||||
@blur="saveSetting(logo.key, settings[logo.key])"
|
||||
:disabled="saving"
|
||||
>
|
||||
<div class="map-upload-row">
|
||||
<input type="file" :accept="logo.accept" @change="uploadLogo(logo.kind, logo.key, $event)" :disabled="brandingUploading" />
|
||||
<img v-if="settings[logo.key]" :src="settings[logo.key]" class="map-thumb" :alt="logo.label" />
|
||||
</div>
|
||||
<small class="input-hint">{{ logo.hint }}</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Primary brand color</span>
|
||||
<div class="color-input-row">
|
||||
<input
|
||||
type="color"
|
||||
:value="settings.brand_primary_color || '#000000'"
|
||||
@input="settings.brand_primary_color = $event.target.value"
|
||||
@change="saveSetting('brand_primary_color', settings.brand_primary_color)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.brand_primary_color"
|
||||
placeholder="(blank = built-in)"
|
||||
@blur="saveSetting('brand_primary_color', settings.brand_primary_color)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</div>
|
||||
<small class="input-hint">Hex color for the primary accent. Leave blank to use the built-in palette.</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Primary hover color</span>
|
||||
<div class="color-input-row">
|
||||
<input
|
||||
type="color"
|
||||
:value="settings.brand_primary_dark_color || '#000000'"
|
||||
@input="settings.brand_primary_dark_color = $event.target.value"
|
||||
@change="saveSetting('brand_primary_dark_color', settings.brand_primary_dark_color)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.brand_primary_dark_color"
|
||||
placeholder="(blank = derived from primary)"
|
||||
@blur="saveSetting('brand_primary_dark_color', settings.brand_primary_dark_color)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</div>
|
||||
<small class="input-hint">Hover/active shade of the primary color. Leave blank to auto-darken the primary color ~15%.</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Accent color</span>
|
||||
<div class="color-input-row">
|
||||
<input
|
||||
type="color"
|
||||
:value="settings.brand_accent_color || '#000000'"
|
||||
@input="settings.brand_accent_color = $event.target.value"
|
||||
@change="saveSetting('brand_accent_color', settings.brand_accent_color)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.brand_accent_color"
|
||||
placeholder="(blank = built-in)"
|
||||
@blur="saveSetting('brand_accent_color', settings.brand_accent_color)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</div>
|
||||
<small class="input-hint">Accent for secondary buttons and badges. Leave blank to use the built-in palette.</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Sidebar color</span>
|
||||
<div class="color-input-row">
|
||||
<input
|
||||
type="color"
|
||||
:value="settings.brand_sidebar_color || '#000000'"
|
||||
@input="settings.brand_sidebar_color = $event.target.value"
|
||||
@change="saveSetting('brand_sidebar_color', settings.brand_sidebar_color)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.brand_sidebar_color"
|
||||
placeholder="(blank = built-in)"
|
||||
@blur="saveSetting('brand_sidebar_color', settings.brand_sidebar_color)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</div>
|
||||
<small class="input-hint">Sidebar background color. Leave blank to use the built-in palette.</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useSystemSettings, brandingLogos } from '../../composables/systemSettings'
|
||||
|
||||
const {
|
||||
settings, saving, brandingUploading, error, success,
|
||||
loadSettings, saveSetting, uploadLogo,
|
||||
} = useSystemSettings()
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
117
frontend/src/views/settings/DellWarrantySettings.vue
Normal file
117
frontend/src/views/settings/DellWarrantySettings.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Dell Warranty Lookup</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<p class="setting-description">
|
||||
Look up Dell coverage by service tag via the Dell TechDirect Warranty API.
|
||||
When enabled, the Refresh button on a Dell warranty pulls the current service
|
||||
level and end date. Requires a Dell TechDirect API client id and secret.
|
||||
</p>
|
||||
|
||||
<div class="setting-row">
|
||||
<label class="toggle-label">
|
||||
<span>Enable Dell Warranty Lookup</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: settings.warranty_dell_enabled }"
|
||||
@click="toggleSetting('warranty_dell_enabled')"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row" v-if="settings.warranty_dell_enabled">
|
||||
<label>
|
||||
<span>Client ID</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.warranty_dell_clientid"
|
||||
placeholder="Dell TechDirect client id"
|
||||
@blur="saveSetting('warranty_dell_clientid', settings.warranty_dell_clientid)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row" v-if="settings.warranty_dell_enabled">
|
||||
<label>
|
||||
<span>Client Secret</span>
|
||||
<input
|
||||
type="password"
|
||||
v-model="settings.warranty_dell_clientsecret"
|
||||
placeholder="Enter client secret"
|
||||
@blur="saveSetting('warranty_dell_clientsecret', settings.warranty_dell_clientsecret)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row" v-if="settings.warranty_dell_enabled">
|
||||
<label>
|
||||
<span>Token URL <small>(blank = Dell default)</small></span>
|
||||
<input
|
||||
type="url"
|
||||
v-model="settings.warranty_dell_tokenurl"
|
||||
placeholder="https://apigtwb2c.us.dell.com/auth/oauth/v2/token"
|
||||
@blur="saveSetting('warranty_dell_tokenurl', settings.warranty_dell_tokenurl)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row" v-if="settings.warranty_dell_enabled">
|
||||
<label>
|
||||
<span>API URL <small>(blank = Dell default)</small></span>
|
||||
<input
|
||||
type="url"
|
||||
v-model="settings.warranty_dell_apiurl"
|
||||
placeholder="https://apigtwb2c.us.dell.com/PROD/sbil/eapi/v5/asset-entitlements"
|
||||
@blur="saveSetting('warranty_dell_apiurl', settings.warranty_dell_apiurl)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="status-indicator" v-if="settings.warranty_dell_enabled">
|
||||
<span class="status-dot" :class="dellStatus"></span>
|
||||
<span>{{ dellMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { useSystemSettings } from '../../composables/systemSettings'
|
||||
|
||||
const {
|
||||
settings, saving, error, success,
|
||||
loadSettings, saveSetting, toggleSetting,
|
||||
} = useSystemSettings()
|
||||
|
||||
// Per-page connection status (stays with the page, not the composable).
|
||||
const dellStatus = computed(() => {
|
||||
if (!settings.warranty_dell_enabled) return 'inactive'
|
||||
if (!settings.warranty_dell_clientid || !settings.warranty_dell_clientsecret) return 'warning'
|
||||
return 'pending'
|
||||
})
|
||||
|
||||
const dellMessage = computed(() => {
|
||||
if (!settings.warranty_dell_enabled) return 'Disabled'
|
||||
if (!settings.warranty_dell_clientid) return 'Client id not configured'
|
||||
if (!settings.warranty_dell_clientsecret) return 'Client secret not configured'
|
||||
return 'Configured (used when you refresh a Dell warranty)'
|
||||
})
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
208
frontend/src/views/settings/EmailSettings.vue
Normal file
208
frontend/src/views/settings/EmailSettings.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Email / SMTP</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<h3>Email Notifications</h3>
|
||||
<p class="setting-description">
|
||||
Configure SMTP settings to enable email notifications for alerts, toner reports,
|
||||
and other system notifications.
|
||||
</p>
|
||||
|
||||
<div class="setting-row">
|
||||
<label class="toggle-label">
|
||||
<span>Enable Email Notifications</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: settings.smtp_enabled }"
|
||||
@click="toggleSetting('smtp_enabled')"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<template v-if="settings.smtp_enabled">
|
||||
<div class="settings-grid">
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>SMTP Host</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.smtp_host"
|
||||
placeholder="smtp.example.com"
|
||||
@blur="saveSetting('smtp_host', settings.smtp_host)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>SMTP Port</span>
|
||||
<input
|
||||
type="number"
|
||||
v-model="settings.smtp_port"
|
||||
placeholder="587"
|
||||
@blur="saveSetting('smtp_port', settings.smtp_port)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Username</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.smtp_username"
|
||||
placeholder="username"
|
||||
@blur="saveSetting('smtp_username', settings.smtp_username)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Password</span>
|
||||
<input
|
||||
type="password"
|
||||
v-model="settings.smtp_password"
|
||||
placeholder="password"
|
||||
@blur="saveSetting('smtp_password', settings.smtp_password)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label class="toggle-label">
|
||||
<span>Use TLS Encryption</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: settings.smtp_use_tls }"
|
||||
@click="toggleSetting('smtp_use_tls')"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-grid">
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>From Address</span>
|
||||
<input
|
||||
type="email"
|
||||
v-model="settings.smtp_from_address"
|
||||
placeholder="noreply@example.com"
|
||||
@blur="saveSetting('smtp_from_address', settings.smtp_from_address)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>From Name</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.smtp_from_name"
|
||||
placeholder="ShopDB"
|
||||
@blur="saveSetting('smtp_from_name', settings.smtp_from_name)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row full-width">
|
||||
<label>
|
||||
<span>Default Alert Recipients</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.alert_recipients"
|
||||
placeholder="user1@example.com, user2@example.com"
|
||||
@blur="saveSetting('alert_recipients', settings.alert_recipients)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">Comma-separated list of email addresses</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot" :class="smtpStatus"></span>
|
||||
<span>{{ smtpMessage }}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="test-btn"
|
||||
@click="testEmail"
|
||||
:disabled="saving || testingEmail || !canTestEmail"
|
||||
>
|
||||
{{ testingEmail ? 'Sending...' : 'Send Test Email' }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import { useSystemSettings } from '../../composables/systemSettings'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
|
||||
const {
|
||||
settings, saving, error, success,
|
||||
loadSettings, saveSetting, toggleSetting,
|
||||
} = useSystemSettings()
|
||||
|
||||
const testingEmail = ref(false)
|
||||
|
||||
// Per-page connection status (stays with the page, not the composable).
|
||||
const smtpStatus = computed(() => {
|
||||
if (!settings.smtp_enabled) return 'inactive'
|
||||
if (!settings.smtp_host || !settings.smtp_from_address) return 'warning'
|
||||
return 'pending'
|
||||
})
|
||||
|
||||
const smtpMessage = computed(() => {
|
||||
if (!settings.smtp_enabled) return 'Disabled'
|
||||
if (!settings.smtp_host) return 'SMTP host not configured'
|
||||
if (!settings.smtp_from_address) return 'From address not configured'
|
||||
return 'Configured - use test button to verify'
|
||||
})
|
||||
|
||||
const canTestEmail = computed(() => {
|
||||
return settings.smtp_host && settings.smtp_from_address && settings.alert_recipients
|
||||
})
|
||||
|
||||
async function testEmail() {
|
||||
// TODO: Implement test email endpoint
|
||||
testingEmail.value = true
|
||||
error.value = ''
|
||||
success.value = ''
|
||||
try {
|
||||
// await settingsApi.testEmail()
|
||||
success.value = 'Test email feature coming soon'
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'Failed to send test email')
|
||||
} finally {
|
||||
testingEmail.value = false
|
||||
setTimeout(() => { success.value = '' }, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
@@ -13,7 +13,7 @@
|
||||
The directory is in <strong>external</strong> mode - people come from a
|
||||
separate HR database, so there is nothing to manage here. To manage
|
||||
people in-app, set <code>employee_directory_mode</code> to
|
||||
<code>selfhosted</code> under System Settings, then reload.
|
||||
<code>selfhosted</code> under Site & Facility settings, then reload.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
100
frontend/src/views/settings/FloorMapSettings.vue
Normal file
100
frontend/src/views/settings/FloorMapSettings.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Floor Map</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<h3>Facility Blueprint</h3>
|
||||
<p class="setting-description">
|
||||
The floor-plan image and its pixel dimensions for this facility. Map
|
||||
markers are positioned against these dimensions, so the width and
|
||||
height must match the native size of the blueprint image. Leave the
|
||||
image paths at their defaults to use the bundled sitemap.
|
||||
</p>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Blueprint image (light theme)</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.map_blueprint_light"
|
||||
placeholder="/static/images/sitemap2025-light.png"
|
||||
@blur="saveSetting('map_blueprint_light', settings.map_blueprint_light)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<div class="map-upload-row">
|
||||
<input type="file" accept="image/*" @change="uploadBlueprint('light', $event)" :disabled="mapUploading" />
|
||||
<img v-if="settings.map_blueprint_light" :src="settings.map_blueprint_light" class="map-thumb" alt="light blueprint" />
|
||||
</div>
|
||||
<small class="input-hint">Upload an image, or type a path/URL to the light-theme floor plan</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Blueprint image (dark theme)</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.map_blueprint_dark"
|
||||
placeholder="/static/images/sitemap2025-dark.png"
|
||||
@blur="saveSetting('map_blueprint_dark', settings.map_blueprint_dark)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<div class="map-upload-row">
|
||||
<input type="file" accept="image/*" @change="uploadBlueprint('dark', $event)" :disabled="mapUploading" />
|
||||
<img v-if="settings.map_blueprint_dark" :src="settings.map_blueprint_dark" class="map-thumb map-thumb-dark" alt="dark blueprint" />
|
||||
</div>
|
||||
<small class="input-hint">Upload an image, or type a path/URL to the dark-theme floor plan</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Blueprint width (pixels)</span>
|
||||
<input
|
||||
type="number"
|
||||
v-model="settings.map_width"
|
||||
min="1"
|
||||
placeholder="3300"
|
||||
@blur="saveSetting('map_width', settings.map_width)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">Native pixel width of the blueprint image</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Blueprint height (pixels)</span>
|
||||
<input
|
||||
type="number"
|
||||
v-model="settings.map_height"
|
||||
min="1"
|
||||
placeholder="2550"
|
||||
@blur="saveSetting('map_height', settings.map_height)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">Native pixel height of the blueprint image</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useSystemSettings } from '../../composables/systemSettings'
|
||||
|
||||
const {
|
||||
settings, saving, mapUploading, error, success,
|
||||
loadSettings, saveSetting, uploadBlueprint,
|
||||
} = useSystemSettings()
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
45
frontend/src/views/settings/GlobalSearchSettings.vue
Normal file
45
frontend/src/views/settings/GlobalSearchSettings.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Global Search</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<p class="setting-description">
|
||||
Choose which content types appear in global search results. Disabling a
|
||||
type here hides it from search without disabling the plugin elsewhere.
|
||||
</p>
|
||||
|
||||
<div class="setting-row" v-for="domain in searchDomains" :key="domain.key">
|
||||
<label class="toggle-label">
|
||||
<span>{{ domain.label }}</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: searchValue(domain.key) }"
|
||||
@click="toggleSearchDomain(domain.key)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useSystemSettings, searchDomains } from '../../composables/systemSettings'
|
||||
|
||||
const {
|
||||
saving, error, success,
|
||||
loadSettings, searchValue, toggleSearchDomain,
|
||||
} = useSystemSettings()
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
58
frontend/src/views/settings/PCTypeMappingSettings.vue
Normal file
58
frontend/src/views/settings/PCTypeMappingSettings.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Collector PC Types</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<p class="setting-description">
|
||||
When the collector ingests a PC, its imaging pc-type (from
|
||||
C:\Enrollment\pc-type.txt) is mapped to one of your Computer Types.
|
||||
Adjust the mapping per site.
|
||||
</p>
|
||||
|
||||
<div class="table-container" v-if="pcTypeMappings.length">
|
||||
<table class="identifier-matrix">
|
||||
<thead>
|
||||
<tr><th>Imaging pc-type</th><th>Computer Type</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in pcTypeMappings" :key="row.pxetype">
|
||||
<td class="identifier-name">{{ row.pxetype }}</td>
|
||||
<td>
|
||||
<select
|
||||
:value="row.computertype"
|
||||
@change="changePcTypeMapping(row.pxetype, $event.target.value)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<option v-for="ct in computerTypes" :key="ct" :value="ct">{{ ct }}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="setting-description">No collector pc-type mappings are configured yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useSystemSettings } from '../../composables/systemSettings'
|
||||
|
||||
const {
|
||||
saving, error, success, pcTypeMappings, computerTypes,
|
||||
loadSettings, loadComputerTypes, changePcTypeMapping,
|
||||
} = useSystemSettings()
|
||||
|
||||
onMounted(() => {
|
||||
loadSettings()
|
||||
loadComputerTypes()
|
||||
})
|
||||
</script>
|
||||
75
frontend/src/views/settings/PrintingSettings.vue
Normal file
75
frontend/src/views/settings/PrintingSettings.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Printing & Labels</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<p class="setting-description">
|
||||
Where printed QR codes point. Leave a target blank to link to the
|
||||
asset's own page on this instance, or enter a custom URL template
|
||||
with {placeholder} substitution.
|
||||
</p>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Printer QR target</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.qr_target_printer"
|
||||
placeholder="(blank = printer page)"
|
||||
@blur="saveSetting('qr_target_printer', settings.qr_target_printer)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">Placeholders: {printerid}, {assetid}, {assetnumber}, {serialnumber}, {ip}, {hostname}</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>USB label QR target</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.qr_target_usb"
|
||||
placeholder="(blank = USB device page)"
|
||||
@blur="saveSetting('qr_target_usb', settings.qr_target_usb)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">Placeholders: {id}, {serialnumber}, {alias}</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>USB label style</span>
|
||||
<select
|
||||
v-model="settings.usb_label_style"
|
||||
@change="saveSetting('usb_label_style', settings.usb_label_style)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<option value="barcode">Barcode (CODE128 of the serial number)</option>
|
||||
<option value="qr">QR code (links to the USB label QR target)</option>
|
||||
</select>
|
||||
<small class="input-hint">Applies to the batch USB mini-label print sheet.</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useSystemSettings } from '../../composables/systemSettings'
|
||||
|
||||
const {
|
||||
settings, saving, error, success,
|
||||
loadSettings, saveSetting,
|
||||
} = useSystemSettings()
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
105
frontend/src/views/settings/ServiceNowSettings.vue
Normal file
105
frontend/src/views/settings/ServiceNowSettings.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>ServiceNow</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<p class="setting-description">
|
||||
Wire global search and ticket links to your ServiceNow instance. When
|
||||
enabled, matching ticket numbers become clickable links and can trigger
|
||||
a smart-redirect from global search. Defaults ship for GE; change them
|
||||
for your site.
|
||||
</p>
|
||||
|
||||
<div class="setting-row">
|
||||
<label class="toggle-label">
|
||||
<span>Enable ServiceNow</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: settings.servicenow_enabled }"
|
||||
@click="toggleSetting('servicenow_enabled')"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<template v-if="settings.servicenow_enabled">
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Search URL</span>
|
||||
<input
|
||||
type="url"
|
||||
v-model="settings.servicenow_search_url"
|
||||
placeholder="https://geaerospaceqa.service-now.com/now/nav/ui/search/.../{ticket}/..."
|
||||
@blur="saveSetting('servicenow_search_url', settings.servicenow_search_url)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">Global-search redirect target. Use {ticket} where the ticket number goes.</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Ticket Prefixes</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="settings.servicenow_ticket_prefixes"
|
||||
placeholder="GEINC,GECHG,GERIT,GESCT"
|
||||
@blur="saveSetting('servicenow_ticket_prefixes', settings.servicenow_ticket_prefixes)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">Comma-separated ticket-number prefixes that this site recognizes.</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Incident URL</span>
|
||||
<input
|
||||
type="url"
|
||||
v-model="settings.servicenow_incident_url"
|
||||
placeholder="(blank = plain text; use {ticket} in a URL template)"
|
||||
@blur="saveSetting('servicenow_incident_url', settings.servicenow_incident_url)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">Link template for incident tickets. Use {ticket} where the ticket number goes.</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>
|
||||
<span>Change URL</span>
|
||||
<input
|
||||
type="url"
|
||||
v-model="settings.servicenow_change_url"
|
||||
placeholder="(blank = plain text; use {ticket} in a URL template)"
|
||||
@blur="saveSetting('servicenow_change_url', settings.servicenow_change_url)"
|
||||
:disabled="saving"
|
||||
>
|
||||
<small class="input-hint">Link template for change tickets. Use {ticket} where the ticket number goes.</small>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useSystemSettings } from '../../composables/systemSettings'
|
||||
|
||||
const {
|
||||
settings, saving, error, success,
|
||||
loadSettings, saveSetting, toggleSetting,
|
||||
} = useSystemSettings()
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
@@ -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
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
90
frontend/src/views/settings/ZabbixSettings.vue
Normal file
90
frontend/src/views/settings/ZabbixSettings.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Zabbix Supplies</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="setting-group">
|
||||
<p class="setting-description">
|
||||
Connect to Zabbix for real-time printer supply monitoring. When enabled, supply levels
|
||||
are fetched from Zabbix API and displayed on printer detail pages.
|
||||
</p>
|
||||
|
||||
<div class="setting-row">
|
||||
<label class="toggle-label">
|
||||
<span>Enable Zabbix Integration</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: settings.zabbix_enabled }"
|
||||
@click="toggleSetting('zabbix_enabled')"
|
||||
:disabled="saving"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row" v-if="settings.zabbix_enabled">
|
||||
<label>
|
||||
<span>Zabbix API URL</span>
|
||||
<input
|
||||
type="url"
|
||||
v-model="settings.zabbix_url"
|
||||
placeholder="http://zabbix.example.com:8080"
|
||||
@blur="saveSetting('zabbix_url', settings.zabbix_url)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row" v-if="settings.zabbix_enabled">
|
||||
<label>
|
||||
<span>Zabbix API Token</span>
|
||||
<input
|
||||
type="password"
|
||||
v-model="settings.zabbix_token"
|
||||
placeholder="Enter API token"
|
||||
@blur="saveSetting('zabbix_token', settings.zabbix_token)"
|
||||
:disabled="saving"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="status-indicator" v-if="settings.zabbix_enabled">
|
||||
<span class="status-dot" :class="zabbixStatus"></span>
|
||||
<span>{{ zabbixMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="success" class="settings-success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { useSystemSettings } from '../../composables/systemSettings'
|
||||
|
||||
const {
|
||||
settings, saving, error, success,
|
||||
loadSettings, saveSetting, toggleSetting,
|
||||
} = useSystemSettings()
|
||||
|
||||
// Per-page connection status (stays with the page, not the composable).
|
||||
const zabbixStatus = computed(() => {
|
||||
if (!settings.zabbix_enabled) return 'inactive'
|
||||
if (!settings.zabbix_url || !settings.zabbix_token) return 'warning'
|
||||
return 'pending'
|
||||
})
|
||||
|
||||
const zabbixMessage = computed(() => {
|
||||
if (!settings.zabbix_enabled) return 'Disabled'
|
||||
if (!settings.zabbix_url) return 'URL not configured'
|
||||
if (!settings.zabbix_token) return 'Token not configured'
|
||||
return 'Configured (connectivity checked on first use)'
|
||||
})
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user