This local working tree had accumulated a substantial amount of finished-but-never-committed frontend work: custom client fields (replacing the old static ClientFields with a dynamic ClientCustomFieldCard + useCustomFields), CSV import (useImport + settings/import page), history type management (useHistoryTypes + settings/history-types page), an admin-only route middleware, several new icon components, a shared useApiFetch composable, and a client detail page restructure ([id].vue -> [id]/index.vue). None of it had ever been pushed, so production was running a stale build that was already linking to routes/components that didn't exist server-side (missing IconSettings, 404s on /settings/import, etc). This commit brings the repo in line with local state.
113 lines
3.7 KiB
Vue
113 lines
3.7 KiB
Vue
<script setup lang="ts">
|
|
import type { ClientCustomField } from '~/types'
|
|
|
|
const props = defineProps<{ field: ClientCustomField }>()
|
|
const emit = defineEmits<{ save: [definitionId: number, value: string | null] }>()
|
|
|
|
const editing = ref(false)
|
|
const saving = ref(false)
|
|
const draft = ref<string>(props.field.value ?? '')
|
|
|
|
// Keep the draft in sync if the field reloads from the server.
|
|
watch(() => props.field.value, (v) => {
|
|
if (!editing.value) draft.value = v ?? ''
|
|
})
|
|
|
|
const startEdit = () => {
|
|
draft.value = props.field.value ?? ''
|
|
editing.value = true
|
|
}
|
|
|
|
const cancel = () => {
|
|
draft.value = props.field.value ?? ''
|
|
editing.value = false
|
|
}
|
|
|
|
const save = async () => {
|
|
saving.value = true
|
|
try {
|
|
const next = draft.value === '' ? null : draft.value
|
|
emit('save', props.field.definition_id, next)
|
|
editing.value = false
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
const formatDate = (iso: string) =>
|
|
new Date(iso).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' })
|
|
|
|
// Human-readable value for display (dates rendered in German short form).
|
|
const displayValue = computed(() => {
|
|
const v = props.field.value
|
|
if (v === null || v === '') return null
|
|
if (props.field.type === 'date') {
|
|
const d = new Date(v)
|
|
return Number.isNaN(d.getTime()) ? v : d.toLocaleDateString('de-DE', { dateStyle: 'medium' })
|
|
}
|
|
return v
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="border border-gray-200 bg-white p-4">
|
|
<div class="flex items-start justify-between gap-2">
|
|
<h3 class="text-sm font-semibold text-gray-800">{{ field.label }}</h3>
|
|
<button
|
|
v-if="!editing"
|
|
@click="startEdit"
|
|
:title="`${field.label} bearbeiten`"
|
|
class="shrink-0 text-gray-400 hover:text-blue-600"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
|
<path d="M2.695 14.762l-1.262 3.155a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.886L17.5 5.501a2.121 2.121 0 00-3-3L3.58 13.419a4 4 0 00-.885 1.343z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Value display -->
|
|
<div v-if="!editing" class="mt-1">
|
|
<p v-if="displayValue !== null" class="text-sm text-gray-800 whitespace-pre-wrap">{{ displayValue }}</p>
|
|
<p v-else class="text-sm text-gray-400 italic">Nicht gesetzt</p>
|
|
</div>
|
|
|
|
<!-- Inline editor (type-aware) -->
|
|
<div v-else class="mt-2 space-y-2">
|
|
<textarea
|
|
v-if="field.type === 'textarea'"
|
|
v-model="draft"
|
|
rows="4"
|
|
class="w-full border border-gray-300 px-3 py-2 text-sm resize-y"
|
|
/>
|
|
<select
|
|
v-else-if="field.type === 'select'"
|
|
v-model="draft"
|
|
class="w-full border border-gray-300 px-3 py-2 text-sm"
|
|
>
|
|
<option value="">— Bitte wählen —</option>
|
|
<option v-for="opt in field.options" :key="opt" :value="opt">{{ opt }}</option>
|
|
</select>
|
|
<input
|
|
v-else
|
|
v-model="draft"
|
|
:type="field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'"
|
|
class="w-full border border-gray-300 px-3 py-2 text-sm"
|
|
/>
|
|
<div class="flex gap-2">
|
|
<button
|
|
@click="save"
|
|
:disabled="saving"
|
|
class="bg-blue-600 text-white px-3 py-1 text-sm hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
Speichern
|
|
</button>
|
|
<button @click="cancel" class="text-sm text-gray-500 hover:underline px-2">Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metadata: last change date + author -->
|
|
<p v-if="!editing && field.updated_at" class="mt-2 text-xs text-gray-400">
|
|
{{ formatDate(field.updated_at) }}<template v-if="field.author_name"> · {{ field.author_name }}</template>
|
|
</p>
|
|
</div>
|
|
</template>
|