printedparts stage 12: admin settings page + settings-rail card
PrintedPartsSettings edits the four plugin settings (code prefix, default threshold, kiosk badge policy, alert recipients) through the core settings API; the route rides the plugin's router file and the settings shell nests it into the rail; get_settings_cards contributes the catalog card while the plugin is enabled.
This commit is contained in:
@@ -306,6 +306,20 @@ CONTRACT ADDITION, because the mailer was not on the plugin surface:
|
|||||||
crossing, no re-fire while below, rearm after restock (see
|
crossing, no re-fire while below, rearm after restock (see
|
||||||
`test_lowstock_alert_fires_on_crossing_only`).
|
`test_lowstock_alert_fires_on_crossing_only`).
|
||||||
|
|
||||||
|
## Stage 12 (extension) - the admin settings page
|
||||||
|
|
||||||
|
A get_settings_cards card needs a PAGE to link, which is why stage 9 deferred
|
||||||
|
it. The page is ordinary:
|
||||||
|
1. `frontend/src/views/settings/PrintedPartsSettings.vue` - load the four
|
||||||
|
keys via `settingsApi.list({category: 'printedparts'})`, save each with
|
||||||
|
`settingsApi.update(key, value)` (admin-gated server-side).
|
||||||
|
2. Route in the PLUGIN's router file with path `settings/printedparts` +
|
||||||
|
`requiresAuth, requiresAdmin, plugin` meta - the router shell
|
||||||
|
automatically nests any `settings/...` path under the two-pane settings
|
||||||
|
rail.
|
||||||
|
3. `get_settings_cards` on the plugin pointing at `/settings/printedparts` -
|
||||||
|
the card appears in the rail's catalog while the plugin is enabled.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Where each pattern lives (cheat sheet)
|
## Where each pattern lives (cheat sheet)
|
||||||
|
|||||||
@@ -31,5 +31,11 @@ export default [
|
|||||||
name: 'printedparts-edit',
|
name: 'printedparts-edit',
|
||||||
component: () => import('../../views/printedparts/PrintedItemForm.vue'),
|
component: () => import('../../views/printedparts/PrintedItemForm.vue'),
|
||||||
meta: { requiresAuth: true, plugin: 'printedparts' }
|
meta: { requiresAuth: true, plugin: 'printedparts' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings/printedparts',
|
||||||
|
name: 'settings-printedparts',
|
||||||
|
component: () => import('../../views/settings/PrintedPartsSettings.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'printedparts' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
109
frontend/src/views/settings/PrintedPartsSettings.vue
Normal file
109
frontend/src/views/settings/PrintedPartsSettings.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>3D Printed Parts</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card form-card">
|
||||||
|
<div v-if="message" class="settings-success">{{ message }}</div>
|
||||||
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Item code prefix</label>
|
||||||
|
<input v-model="values.printedparts_code_prefix" type="text"
|
||||||
|
class="form-control" maxlength="8" />
|
||||||
|
<p class="field-hint">
|
||||||
|
New items mint codes like {{ values.printedparts_code_prefix || '3DP' }}-0042.
|
||||||
|
Changing it does not rename existing items.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Default low-stock threshold</label>
|
||||||
|
<input v-model.number="values.printedparts_default_threshold"
|
||||||
|
type="number" min="0" class="form-control" />
|
||||||
|
<p class="field-hint">Seed value for new items; each item can override.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Unknown badge at the kiosk</label>
|
||||||
|
<select v-model="values.printedparts_unknown_badge" class="form-control">
|
||||||
|
<option value="deny">Deny - refuse badges with no directory match</option>
|
||||||
|
<option value="allow">Allow - record the SSO with no name</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Low-stock alert recipients</label>
|
||||||
|
<input v-model="values.printedparts_alert_email" type="text"
|
||||||
|
class="form-control" placeholder="parts-team@example.com, lead@example.com" />
|
||||||
|
<p class="field-hint">
|
||||||
|
Comma-separated. Empty uses the site-wide alert recipients
|
||||||
|
(Settings > System > Email). Alerts fire once when an item
|
||||||
|
crosses its threshold; restocking above re-arms.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" :disabled="saving" @click="save">
|
||||||
|
{{ saving ? 'Saving...' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { settingsApi } from '@/api'
|
||||||
|
|
||||||
|
const KEYS = [
|
||||||
|
'printedparts_code_prefix',
|
||||||
|
'printedparts_default_threshold',
|
||||||
|
'printedparts_unknown_badge',
|
||||||
|
'printedparts_alert_email'
|
||||||
|
]
|
||||||
|
|
||||||
|
const values = ref({
|
||||||
|
printedparts_code_prefix: '3DP',
|
||||||
|
printedparts_default_threshold: 5,
|
||||||
|
printedparts_unknown_badge: 'deny',
|
||||||
|
printedparts_alert_email: ''
|
||||||
|
})
|
||||||
|
const saving = ref(false)
|
||||||
|
const message = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const response = await settingsApi.list({ category: 'printedparts' })
|
||||||
|
const rows = response.data.data || []
|
||||||
|
for (const row of rows) {
|
||||||
|
if (KEYS.includes(row.key)) values.value[row.key] = row.value
|
||||||
|
}
|
||||||
|
values.value.printedparts_default_threshold =
|
||||||
|
parseInt(values.value.printedparts_default_threshold, 10) || 0
|
||||||
|
} catch (loadError) {
|
||||||
|
error.value = 'Could not load settings'
|
||||||
|
console.error(loadError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
saving.value = true
|
||||||
|
message.value = ''
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
for (const key of KEYS) {
|
||||||
|
await settingsApi.update(key, String(values.value[key] ?? ''))
|
||||||
|
}
|
||||||
|
message.value = 'Settings saved'
|
||||||
|
} catch (saveError) {
|
||||||
|
error.value = saveError.response?.data?.data?.error?.message || 'Save failed'
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.field-hint { color: var(--text-light); font-size: 0.85rem; margin-top: 0.25rem; }
|
||||||
|
</style>
|
||||||
@@ -62,6 +62,19 @@ class PrintedpartsPlugin(BasePlugin):
|
|||||||
'printedparts'),
|
'printedparts'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_settings_cards(self) -> List[dict]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'group': '3D Printed Parts',
|
||||||
|
'to': '/settings/printedparts',
|
||||||
|
'icon': 'box',
|
||||||
|
'title': '3D Parts Settings',
|
||||||
|
'description': 'Item code prefix, default threshold, kiosk '
|
||||||
|
'badge policy, low-stock alert recipients',
|
||||||
|
'position': 47,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
def get_reports(self) -> List[dict]:
|
def get_reports(self) -> List[dict]:
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user