mandant-crm/frontend/pages/settings/fields.vue

196 lines
7.2 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import type { CustomFieldDefinition, CustomFieldType } from '~/types'
import type { CustomFieldDefinitionPayload } from '~/composables/useCustomFields'
definePageMeta({ middleware: ['auth', 'admin'] })
const { definitions, fetchDefinitions, createDefinition, updateDefinition, deleteDefinition, reorderDefinitions } = useCustomFields()
await fetchDefinitions()
const typeLabels: Record<CustomFieldType, string> = {
text: 'Text (einzeilig)',
textarea: 'Text (mehrzeilig)',
number: 'Zahl',
date: 'Datum',
select: 'Auswahl',
}
const emptyForm = (): CustomFieldDefinitionPayload & { optionsText: string } => ({
label: '',
type: 'text',
options: [],
is_required: false,
optionsText: '',
})
const editingId = ref<number | null>(null)
const form = ref(emptyForm())
const error = ref('')
const resetForm = () => {
editingId.value = null
form.value = emptyForm()
error.value = ''
}
const startEdit = (def: CustomFieldDefinition) => {
editingId.value = def.id
error.value = ''
form.value = {
label: def.label,
type: def.type,
options: def.options,
is_required: def.is_required,
optionsText: def.options.join('\n'),
}
}
const submit = async () => {
error.value = ''
const payload: CustomFieldDefinitionPayload = {
label: form.value.label.trim(),
type: form.value.type,
is_required: form.value.is_required,
options: form.value.type === 'select'
? form.value.optionsText.split('\n').map(o => o.trim()).filter(Boolean)
: undefined,
}
if (!payload.label) {
error.value = 'Bitte einen Feldnamen angeben.'
return
}
if (form.value.type === 'select' && !(payload.options && payload.options.length)) {
error.value = 'Auswahlfelder benötigen mindestens eine Option.'
return
}
try {
if (editingId.value) {
await updateDefinition(editingId.value, payload)
} else {
await createDefinition(payload)
}
resetForm()
} catch (e: any) {
error.value = e?.data?.message ?? 'Speichern fehlgeschlagen.'
}
}
const remove = async (def: CustomFieldDefinition) => {
if (!confirm(`Feld „${def.label}“ wirklich löschen? Bestehende Werte bleiben in der Historie erhalten.`)) return
await deleteDefinition(def.id)
if (editingId.value === def.id) resetForm()
}
const move = async (index: number, direction: -1 | 1) => {
const target = index + direction
if (target < 0 || target >= definitions.value.length) return
const ids = definitions.value.map(d => d.id)
;[ids[index], ids[target]] = [ids[target], ids[index]]
await reorderDefinitions(ids)
}
</script>
<template>
<div class="min-h-screen bg-gray-50">
<header class="bg-white border-b border-gray-200 px-6 py-3 flex items-center gap-4">
<NuxtLink
to="/clients"
title="Alle Mandanten"
aria-label="Alle Mandanten"
class="text-gray-500 hover:text-gray-700"
>
<IconArrowLeft />
</NuxtLink>
<h1 class="text-base font-semibold text-gray-800">Stammdatenfelder konfigurieren</h1>
</header>
<div class="max-w-3xl mx-auto py-6 px-6 space-y-6">
<!-- Existing definitions -->
<section class="bg-white border border-gray-200">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 text-left text-xs text-gray-500">
<th class="px-4 py-2 font-medium">Feldname</th>
<th class="px-4 py-2 font-medium">Typ</th>
<th class="px-4 py-2 font-medium">Pflicht</th>
<th class="px-4 py-2 font-medium text-right">Aktionen</th>
</tr>
</thead>
<tbody>
<tr v-for="(def, index) in definitions" :key="def.id" class="border-b border-gray-100">
<td class="px-4 py-2 text-gray-800">{{ def.label }}</td>
<td class="px-4 py-2 text-gray-600">{{ typeLabels[def.type] }}</td>
<td class="px-4 py-2 text-gray-600">{{ def.is_required ? 'Ja' : 'Nein' }}</td>
<td class="px-4 py-2 text-right">
<div class="inline-flex gap-1">
<button @click="move(index, -1)" :disabled="index === 0" title="Nach oben" aria-label="Nach oben" class="p-1 text-gray-400 hover:text-blue-600 disabled:opacity-30 disabled:hover:text-gray-400">
<IconArrowUp />
</button>
<button @click="move(index, 1)" :disabled="index === definitions.length - 1" title="Nach unten" aria-label="Nach unten" class="p-1 text-gray-400 hover:text-blue-600 disabled:opacity-30 disabled:hover:text-gray-400">
<IconArrowDown />
</button>
<button @click="startEdit(def)" title="Bearbeiten" aria-label="Bearbeiten" class="p-1 text-gray-400 hover:text-blue-600">
<IconEdit />
</button>
<button @click="remove(def)" title="Löschen" aria-label="Löschen" class="p-1 text-gray-400 hover:text-red-600">
<IconTrash />
</button>
</div>
</td>
</tr>
<tr v-if="definitions.length === 0">
<td colspan="4" class="px-4 py-6 text-center text-sm text-gray-400 italic">
Noch keine Felder konfiguriert.
</td>
</tr>
</tbody>
</table>
</section>
<!-- Create / edit form -->
<section class="bg-white border border-gray-200 p-4 space-y-3">
<h2 class="text-sm font-semibold text-gray-700">
{{ editingId ? 'Feld bearbeiten' : 'Neues Feld' }}
</h2>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Feldname</label>
<input v-model="form.label" class="w-full border border-gray-300 px-3 py-2 text-sm" placeholder="z. B. Beratungsschwerpunkt" />
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Typ</label>
<select v-model="form.type" class="w-full border border-gray-300 px-3 py-2 text-sm">
<option v-for="(label, value) in typeLabels" :key="value" :value="value">{{ label }}</option>
</select>
</div>
</div>
<div v-if="form.type === 'select'">
<label class="block text-xs font-medium text-gray-600 mb-1">Optionen (eine pro Zeile)</label>
<textarea v-model="form.optionsText" rows="4" class="w-full border border-gray-300 px-3 py-2 text-sm resize-y" placeholder="Steuerrecht&#10;Erbrecht" />
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 text-sm text-gray-700">
<input v-model="form.is_required" type="checkbox" />
Pflichtfeld
</label>
</div>
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
<div class="flex gap-2">
<button @click="submit" class="bg-blue-600 text-white px-4 py-1.5 text-sm hover:bg-blue-700">
{{ editingId ? 'Änderungen speichern' : 'Feld anlegen' }}
</button>
<button v-if="editingId" @click="resetForm" class="text-sm text-gray-500 hover:underline px-2">Abbrechen</button>
</div>
</section>
</div>
</div>
</template>