Add custom fields + warranty plugin, rework settings into two-pane shell
Feature work from the 2026-07 session: Settings IA - Replace the flat 27-card settings hub with a persistent two-pane shell (SettingsLayout.vue): grouped, searchable left rail + content pane. - Nest all settings/* routes under the shell via router post-processing; shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers, Equipment, Network) so per-type settings stop scattering. Custom fields (core) - customfields + customfieldvalues tables (migration 7d14), CRUD API at /api/customfields, per-asset value get/save. - Settings management page + reusable CustomFieldsSection (detail) and CustomFieldsInputs (form) wired into all four asset types. Warranty (new plugin) - plugins/warranty: warranties + warrantyassets (migration 7d15), derived coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs). - API CRUD + per-asset panel + report buckets; WarrantyPanel on all four detail pages; Warranties management page; Warranty report + Reports card. - Seed warranty.* permissions. Printer drivers - printerdrivers table (migration 7d13) linked to printer models; drivers now surface on the matching printer's detail page. Other - PCDetail rebalanced (Network + Status + Warranty + custom fields on the right). - Rename PCs list "Features" column to "Remote Access"; fix badge hover underline. - Drop equipment islocationonly field. - Centralize asset-type label/route maps into utils/assetTypes.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
82
frontend/src/components/CustomFieldsInputs.vue
Normal file
82
frontend/src/components/CustomFieldsInputs.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div v-if="fields.length" class="custom-fields">
|
||||
<div class="form-group" v-for="f in fields" :key="f.fieldid">
|
||||
<label>{{ f.label }}</label>
|
||||
|
||||
<select v-if="f.datatype === 'select'" v-model="local[f.fieldid]" class="form-control">
|
||||
<option value="">-- none --</option>
|
||||
<option v-for="opt in f.options" :key="opt" :value="opt">{{ opt }}</option>
|
||||
</select>
|
||||
|
||||
<label v-else-if="f.datatype === 'boolean'" class="checkbox-label">
|
||||
<input type="checkbox"
|
||||
:checked="local[f.fieldid] === 'true'"
|
||||
@change="local[f.fieldid] = $event.target.checked ? 'true' : 'false'" />
|
||||
Yes
|
||||
</label>
|
||||
|
||||
<input v-else-if="f.datatype === 'date'" type="date" v-model="local[f.fieldid]" class="form-control" />
|
||||
<input v-else-if="f.datatype === 'number'" type="number" v-model="local[f.fieldid]" class="form-control" />
|
||||
<input v-else type="text" v-model="local[f.fieldid]" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { customFieldsApi } from '../api'
|
||||
|
||||
const props = defineProps({
|
||||
// Asset type whose fields to render (needed even for a brand-new asset).
|
||||
assettypeid: { type: [Number, String], default: null },
|
||||
// Existing asset id, if editing - used to preload stored values.
|
||||
assetid: { type: [Number, String], default: null },
|
||||
})
|
||||
|
||||
const fields = ref([])
|
||||
const local = reactive({})
|
||||
|
||||
async function load() {
|
||||
fields.value = []
|
||||
Object.keys(local).forEach(k => delete local[k])
|
||||
if (!props.assettypeid) return
|
||||
try {
|
||||
// Definitions for this asset type (form-visible, active).
|
||||
const defsResponse = await customFieldsApi.list({ assettypeid: props.assettypeid })
|
||||
const defs = (defsResponse.data.data || []).filter(f => f.showonform)
|
||||
fields.value = defs
|
||||
|
||||
// Seed blanks, then overlay stored values if editing.
|
||||
for (const f of defs) local[f.fieldid] = f.datatype === 'boolean' ? 'false' : ''
|
||||
if (props.assetid) {
|
||||
const valResponse = await customFieldsApi.forAsset(props.assetid)
|
||||
for (const f of (valResponse.data.data || [])) {
|
||||
if (f.value != null) local[f.fieldid] = String(f.value)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading custom field defs:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Persist current values against an asset id (parent calls after asset save).
|
||||
async function save(assetid) {
|
||||
if (!assetid || !fields.value.length) return
|
||||
const values = {}
|
||||
for (const f of fields.value) values[f.fieldid] = local[f.fieldid]
|
||||
await customFieldsApi.saveForAsset(assetid, values)
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
watch(() => [props.assettypeid, props.assetid], load)
|
||||
|
||||
defineExpose({ save, hasFields: () => fields.value.length > 0 })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user