Files
shopdb-flask/frontend/src/components/CustomFieldsInputs.vue
cproudlock 78a0ee8d83 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>
2026-07-09 15:37:21 -04:00

83 lines
2.8 KiB
Vue

<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>