pp-planer/resources/js/Pages/Settings.vue
2026-03-29 12:10:46 +02:00

231 lines
11 KiB
Vue

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import { Head } from '@inertiajs/vue3'
import { ref, reactive } from 'vue'
const props = defineProps({
settings: {
type: Object,
default: () => ({}),
},
})
const fields = [
// Macro configuration fields
{ key: 'macro_name', label: 'Makro-Name', placeholder: 'z.B. Copyright Makro', section: 'macro' },
{ key: 'macro_uuid', label: 'Makro-UUID', placeholder: 'z.B. 11111111-2222-3333-4444-555555555555', section: 'macro' },
{ key: 'macro_collection_name', label: 'Collection-Name', defaultValue: '--MAIN--', section: 'macro' },
{ key: 'macro_collection_uuid', label: 'Collection-UUID', defaultValue: '8D02FC57-83F8-4042-9B90-81C229728426', section: 'macro' },
// Agenda configuration fields
{ key: 'agenda_start_title', label: 'Ablauf-Start', placeholder: 'z.B. Ablauf* oder Beginn*', section: 'agenda' },
{ key: 'agenda_end_title', label: 'Ablauf-Ende', placeholder: 'z.B. Ende* oder Schluss*', section: 'agenda' },
{ key: 'agenda_announcement_position', label: 'Ankündigungen-Position', placeholder: 'z.B. Informationen*,Hinweise*', helpText: 'Komma-getrennte Liste. Das erste passende Element im Ablauf bestimmt, wo die Ankündigungsfolien eingefügt werden. * als Platzhalter.', section: 'agenda' },
{ key: 'agenda_sermon_matching', label: 'Predigt-Erkennung', placeholder: 'z.B. Predigt*,Sermon*', helpText: 'Komma-getrennte Liste. Erkannte Elemente bekommen einen Predigt-Upload-Bereich. * als Platzhalter.', section: 'agenda' },
]
const form = reactive({})
for (const field of fields) {
form[field.key] = props.settings[field.key] ?? field.defaultValue ?? ''
}
const saving = reactive({})
const saved = reactive({})
const errors = reactive({})
async function saveField(key) {
if (saving[key]) return
saving[key] = true
errors[key] = null
saved[key] = false
try {
const response = await fetch(route('settings.update'), {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-XSRF-TOKEN': decodeURIComponent(
document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? '',
),
},
body: JSON.stringify({ key, value: form[key] || null }),
})
if (!response.ok) {
const data = await response.json()
errors[key] = data.message || 'Speichern fehlgeschlagen'
return
}
saved[key] = true
setTimeout(() => { saved[key] = false }, 2000)
} catch {
errors[key] = 'Netzwerkfehler beim Speichern'
} finally {
saving[key] = false
}
}
</script>
<template>
<Head title="Einstellungen" />
<AuthenticatedLayout>
<template #header>
<h2 class="text-xl font-semibold leading-tight text-gray-800">
Einstellungen
</h2>
</template>
<div class="py-8">
<div class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
<div class="overflow-hidden rounded-xl border border-gray-200/80 bg-white shadow-sm">
<div class="border-b border-gray-100 bg-gray-50/50 px-6 py-4">
<h3 class="text-sm font-semibold text-gray-900">
ProPresenter Makro-Konfiguration
</h3>
<p class="mt-1 text-xs text-gray-500">
Diese Einstellungen werden beim Export auf Copyright-Folien als Makro-Aktion angewendet.
</p>
</div>
<div class="divide-y divide-gray-100 px-6">
<div
v-for="field in fields.filter(f => f.section === 'macro')"
:key="field.key"
class="py-5"
>
<label
:for="'setting-' + field.key"
class="block text-sm font-medium text-gray-700"
>
{{ field.label }}
</label>
<div class="relative mt-1.5">
<input
:id="'setting-' + field.key"
:data-testid="'setting-' + field.key"
v-model="form[field.key]"
type="text"
:placeholder="field.placeholder || field.defaultValue || ''"
class="block w-full rounded-lg border-gray-300 text-sm shadow-sm transition-colors focus:border-amber-400 focus:ring-amber-400/40"
:class="{
'border-red-300 focus:border-red-400 focus:ring-red-400/40': errors[field.key],
'border-emerald-300': saved[field.key],
}"
@blur="saveField(field.key)"
/>
<div
v-if="saving[field.key]"
class="absolute inset-y-0 right-0 flex items-center pr-3"
>
<svg class="h-4 w-4 animate-spin text-gray-400" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
</div>
<div
v-else-if="saved[field.key]"
class="absolute inset-y-0 right-0 flex items-center pr-3"
>
<svg class="h-4 w-4 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<p
v-if="errors[field.key]"
class="mt-1.5 text-xs text-red-600"
:data-testid="'error-' + field.key"
>
{{ errors[field.key] }}
</p>
<p
v-if="field.defaultValue"
class="mt-1.5 text-xs text-gray-400"
>
Standard: {{ field.defaultValue }}
</p>
</div>
</div>
</div>
<div class="overflow-hidden rounded-xl border border-gray-200/80 bg-white shadow-sm mt-6">
<div class="border-b border-gray-100 bg-gray-50/50 px-6 py-4">
<h3 class="text-sm font-semibold text-gray-900">Agenda-Konfiguration</h3>
<p class="mt-1 text-xs text-gray-500">Diese Einstellungen steuern, wie der Gottesdienst-Ablauf angezeigt und exportiert wird.</p>
</div>
<div class="divide-y divide-gray-100 px-6">
<div v-for="field in fields.filter(f => f.section === 'agenda')" :key="field.key" class="py-5">
<label
:for="'setting-' + field.key"
class="block text-sm font-medium text-gray-700"
>
{{ field.label }}
</label>
<div class="relative mt-1.5">
<input
:id="'setting-' + field.key"
:data-testid="'setting-' + field.key"
v-model="form[field.key]"
type="text"
:placeholder="field.placeholder || field.defaultValue || ''"
class="block w-full rounded-lg border-gray-300 text-sm shadow-sm transition-colors focus:border-amber-400 focus:ring-amber-400/40"
:class="{
'border-red-300 focus:border-red-400 focus:ring-red-400/40': errors[field.key],
'border-emerald-300': saved[field.key],
}"
@blur="saveField(field.key)"
/>
<div
v-if="saving[field.key]"
class="absolute inset-y-0 right-0 flex items-center pr-3"
>
<svg class="h-4 w-4 animate-spin text-gray-400" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
</div>
<div
v-else-if="saved[field.key]"
class="absolute inset-y-0 right-0 flex items-center pr-3"
>
<svg class="h-4 w-4 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<p
v-if="errors[field.key]"
class="mt-1.5 text-xs text-red-600"
:data-testid="'error-' + field.key"
>
{{ errors[field.key] }}
</p>
<p v-if="field.helpText" class="mt-1.5 text-xs text-gray-400">{{ field.helpText }}</p>
<p
v-if="field.defaultValue"
class="mt-1.5 text-xs text-gray-400"
>
Standard: {{ field.defaultValue }}
</p>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>