Group Site and Facility settings by concept
Fields rendered in arbitrary key order (dualpath toggle first, facility name buried, setup flag mid-list). Now grouped with headers: Identity, Behavior, Naming and Patterns, Data Sources, System - with unknown future keys falling into Other at the end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,18 +8,21 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div v-if="loading" class="muted">Loading...</div>
|
<div v-if="loading" class="muted">Loading...</div>
|
||||||
<div v-else class="form-grid">
|
<div v-else class="form-grid">
|
||||||
<label v-for="s in items" :key="s.key" class="field">
|
<template v-for="group in groupedItems" :key="group.title">
|
||||||
<span>{{ prettyLabel(s.key) }}</span>
|
<h3 class="group-title">{{ group.title }}</h3>
|
||||||
<span v-if="s.valuetype === 'boolean'" class="bool-row">
|
<label v-for="s in group.items" :key="s.key" class="field">
|
||||||
<input type="checkbox" :checked="isTrue(s)" @change="s.value = isTrue(s) ? 'false' : 'true'" />
|
<span>{{ prettyLabel(s.key) }}</span>
|
||||||
<span class="muted">{{ isTrue(s) ? 'Enabled' : 'Disabled' }}</span>
|
<span v-if="s.valuetype === 'boolean'" class="bool-row">
|
||||||
</span>
|
<input type="checkbox" :checked="isTrue(s)" @change="s.value = isTrue(s) ? 'false' : 'true'" />
|
||||||
<select v-else-if="OPTIONS[s.key]" v-model="s.value">
|
<span class="muted">{{ isTrue(s) ? 'Enabled' : 'Disabled' }}</span>
|
||||||
<option v-for="opt in OPTIONS[s.key]" :key="opt" :value="opt">{{ opt }}</option>
|
</span>
|
||||||
</select>
|
<select v-else-if="OPTIONS[s.key]" v-model="s.value">
|
||||||
<input v-else v-model="s.value" type="text" :placeholder="fieldHelp(s.key) ? '' : s.description" />
|
<option v-for="opt in OPTIONS[s.key]" :key="opt" :value="opt">{{ opt }}</option>
|
||||||
<small class="muted">{{ fieldHelp(s.key) || s.description }}</small>
|
</select>
|
||||||
</label>
|
<input v-else v-model="s.value" type="text" :placeholder="fieldHelp(s.key) ? '' : s.description" />
|
||||||
|
<small class="muted">{{ fieldHelp(s.key) || s.description }}</small>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div v-if="!items.length" class="muted">No site settings found.</div>
|
<div v-if="!items.length" class="muted">No site settings found.</div>
|
||||||
|
|
||||||
@@ -36,11 +39,26 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { settingsApi } from '@/api'
|
import { settingsApi } from '@/api'
|
||||||
import { apiError } from '../../utils/apiError'
|
import { apiError } from '../../utils/apiError'
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
|
|
||||||
|
const groupedItems = computed(() => {
|
||||||
|
const byKey = Object.fromEntries(items.value.map(s => [s.key, s]))
|
||||||
|
const placed = new Set()
|
||||||
|
const groups = []
|
||||||
|
for (const group of GROUPS) {
|
||||||
|
const present = group.keys.filter(k => byKey[k])
|
||||||
|
if (!present.length) continue
|
||||||
|
present.forEach(k => placed.add(k))
|
||||||
|
groups.push({ title: group.title, items: present.map(k => byKey[k]) })
|
||||||
|
}
|
||||||
|
const leftover = items.value.filter(s => !placed.has(s.key))
|
||||||
|
if (leftover.length) groups.push({ title: 'Other', items: leftover })
|
||||||
|
return groups
|
||||||
|
})
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const saved = ref(false)
|
const saved = ref(false)
|
||||||
@@ -71,6 +89,16 @@ function isTrue(setting) {
|
|||||||
return String(setting.value).toLowerCase() === 'true'
|
return String(setting.value).toLowerCase() === 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deliberate field order: identity first, behavior, naming templates, data
|
||||||
|
// sources, system flags last. Unknown future keys fall into Other.
|
||||||
|
const GROUPS = [
|
||||||
|
{ title: 'Identity', keys: ['facility_name', 'site_base_url'] },
|
||||||
|
{ title: 'Behavior', keys: ['dualpath_single_machine'] },
|
||||||
|
{ title: 'Naming & Patterns', keys: ['pc_access_domain', 'printer_hostname_template', 'employeeid_pattern'] },
|
||||||
|
{ title: 'Data Sources', keys: ['employee_directory_mode', 'usb_directory_mode'] },
|
||||||
|
{ title: 'System', keys: ['setup_complete'] }
|
||||||
|
]
|
||||||
|
|
||||||
// Inline help for site fields that need more than the stored description.
|
// Inline help for site fields that need more than the stored description.
|
||||||
const HELP = {
|
const HELP = {
|
||||||
employee_directory_mode: 'Where employee records live: selfhosted (managed in this app, with CSV import and photo uploads) or external (a read-only HR database configured via EMPLOYEE_DB_* environment variables).',
|
employee_directory_mode: 'Where employee records live: selfhosted (managed in this app, with CSV import and photo uploads) or external (a read-only HR database configured via EMPLOYEE_DB_* environment variables).',
|
||||||
@@ -150,6 +178,19 @@ onMounted(load)
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
accent-color: var(--primary);
|
accent-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
.group-title {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
.group-title:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
.muted {
|
.muted {
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user