Füge Unterstützung für Kontaktanfragen hinzu, einschließlich neuer Routen und Berechtigungen für Trainer und Vorstand. Aktualisiere E-Mail-Versandlogik, um Anfragen an alle relevanten Empfänger weiterzuleiten.
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 56s

This commit is contained in:
Torsten Schulz (local)
2026-02-26 14:28:54 +01:00
parent ff8c1970df
commit 46c2c14ae8
15 changed files with 612 additions and 54 deletions

View File

@@ -70,6 +70,9 @@
<option value="newsletter">
Newsletter
</option>
<option value="trainer">
Trainer
</option>
</select>
<!-- Approve Button -->
@@ -177,10 +180,11 @@
'bg-red-100 text-red-800': role === 'admin',
'bg-blue-100 text-blue-800': role === 'vorstand',
'bg-green-100 text-green-800': role === 'newsletter',
'bg-amber-100 text-amber-800': role === 'trainer',
'bg-gray-100 text-gray-800': role === 'mitglied'
}"
>
{{ role === 'admin' ? 'Admin' : role === 'vorstand' ? 'Vorstand' : role === 'newsletter' ? 'Newsletter' : 'Mitglied' }}
{{ role === 'admin' ? 'Admin' : role === 'vorstand' ? 'Vorstand' : role === 'newsletter' ? 'Newsletter' : role === 'trainer' ? 'Trainer' : 'Mitglied' }}
</span>
</div>
<button
@@ -280,6 +284,15 @@
>
<span class="ml-2 text-sm text-gray-700">Newsletter</span>
</label>
<label class="flex items-center">
<input
v-model="selectedRoles"
type="checkbox"
value="trainer"
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
>
<span class="ml-2 text-sm text-gray-700">Trainer</span>
</label>
<label class="flex items-center">
<input
v-model="selectedRoles"

View File

@@ -324,6 +324,14 @@
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">E-Mail</label>
<input
v-model="trainer.email"
type="email"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Zusatzinfo</label>
<div class="flex space-x-2">
@@ -668,6 +676,7 @@ const addTrainer = () => {
name: '',
lizenz: '',
schwerpunkt: '',
email: '',
zusatz: ''
})
}

View File

@@ -110,6 +110,27 @@
</p>
</NuxtLink>
<!-- Kontaktanfragen -->
<NuxtLink
to="/cms/kontaktanfragen"
class="bg-white p-6 rounded-xl shadow-lg border border-gray-100 hover:shadow-xl transition-all group"
>
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-emerald-100 rounded-lg flex items-center justify-center group-hover:bg-emerald-600 transition-colors">
<Mail
:size="24"
class="text-emerald-600 group-hover:text-white"
/>
</div>
<h2 class="ml-4 text-xl font-semibold text-gray-900">
Kontaktanfragen
</h2>
</div>
<p class="text-gray-600">
Kontaktformular-Anfragen einsehen und beantworten
</p>
</NuxtLink>
<!-- Startseite -->
<NuxtLink
to="/cms/startseite"
@@ -179,7 +200,7 @@
</template>
<script setup>
import { Newspaper, Calendar, Users, UserCog, Settings, Layout } from 'lucide-vue-next'
import { Newspaper, Calendar, Users, UserCog, Settings, Layout, Mail } from 'lucide-vue-next'
import { ref, onMounted } from 'vue'
const authStore = useAuthStore()

View File

@@ -0,0 +1,229 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-4xl font-display font-bold text-gray-900">
Kontaktanfragen
</h1>
<div class="w-24 h-1 bg-primary-600 mt-4" />
</div>
<button
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
:disabled="isLoading"
@click="loadRequests"
>
{{ isLoading ? 'Lädt...' : 'Aktualisieren' }}
</button>
</div>
<div class="mb-4 flex items-center justify-end">
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
<input
v-model="showAnswered"
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
>
Bearbeitete Anfragen anzeigen
</label>
</div>
<div v-if="isLoading" class="text-center py-12 text-gray-600">
Lade Kontaktanfragen...
</div>
<div v-else-if="filteredRequests.length === 0" class="bg-white rounded-xl shadow p-8 text-center text-gray-600">
{{ showAnswered ? 'Aktuell liegen keine Kontaktanfragen vor.' : 'Aktuell liegen keine offenen Kontaktanfragen vor.' }}
</div>
<div v-else class="space-y-4">
<div
v-for="request in filteredRequests"
:key="request.id"
class="bg-white rounded-xl shadow border border-gray-100"
>
<div class="p-5 border-b border-gray-100 flex items-start justify-between gap-4">
<div>
<p class="text-lg font-semibold text-gray-900">
{{ request.subject }}
</p>
<p class="text-sm text-gray-600">
Von {{ request.name }} ({{ request.email }}){{ request.phone ? ` · ${request.phone}` : '' }}
</p>
<p class="text-xs text-gray-500 mt-1">
Eingegangen: {{ formatDate(request.createdAt) }}
</p>
</div>
<span
class="px-2.5 py-1 rounded-full text-xs font-semibold"
:class="request.status === 'beantwortet' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'"
>
{{ request.status === 'beantwortet' ? 'Beantwortet' : 'Offen' }}
</span>
</div>
<div class="p-5">
<p class="text-gray-800 whitespace-pre-wrap">
{{ request.message }}
</p>
<div v-if="Array.isArray(request.replies) && request.replies.length > 0" class="mt-5 border-t border-gray-100 pt-4">
<h3 class="text-sm font-semibold text-gray-700 mb-2">
Antworten
</h3>
<div class="space-y-2">
<div
v-for="reply in request.replies"
:key="reply.id"
class="bg-gray-50 rounded-lg p-3"
>
<p class="text-xs text-gray-500 mb-1">
{{ formatDate(reply.createdAt) }}{{ reply.responderEmail ? ` · ${reply.responderEmail}` : '' }}
</p>
<p class="text-sm text-gray-800 whitespace-pre-wrap">
{{ reply.message }}
</p>
</div>
</div>
</div>
<div class="mt-4 flex justify-end">
<button
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
@click="openReplyModal(request)"
>
Antworten
</button>
</div>
</div>
</div>
</div>
</div>
<div
v-if="replyModalOpen && selectedRequest"
class="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4"
@click.self="closeReplyModal"
>
<div class="bg-white rounded-xl shadow-2xl max-w-2xl w-full p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-2">
Antwort senden
</h2>
<p class="text-sm text-gray-600 mb-4">
An: {{ selectedRequest.email }}<br>
Betreff: <strong>Aw: {{ selectedRequest.subject }}</strong>
</p>
<textarea
v-model="replyText"
rows="8"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-600"
placeholder="Ihre Antwort..."
/>
<div v-if="errorMessage" class="mt-3 text-sm text-red-600">
{{ errorMessage }}
</div>
<div class="mt-5 flex justify-end gap-3">
<button
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
:disabled="isSendingReply"
@click="closeReplyModal"
>
Abbrechen
</button>
<button
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
:disabled="isSendingReply || !replyText.trim()"
@click="sendReply"
>
{{ isSendingReply ? 'Sende...' : 'Antwort senden' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const requests = ref([])
const isLoading = ref(false)
const replyModalOpen = ref(false)
const selectedRequest = ref(null)
const replyText = ref('')
const isSendingReply = ref(false)
const errorMessage = ref('')
const showAnswered = ref(false)
const filteredRequests = computed(() => {
if (showAnswered.value) return requests.value
return requests.value.filter((request) => request.status !== 'beantwortet')
})
const formatDate = (value) => {
if (!value) return '-'
return new Date(value).toLocaleString('de-DE')
}
const loadRequests = async () => {
isLoading.value = true
try {
requests.value = await $fetch('/api/cms/contact-requests')
} catch (error) {
console.error('Fehler beim Laden der Kontaktanfragen:', error)
requests.value = []
} finally {
isLoading.value = false
}
}
const openReplyModal = (request) => {
selectedRequest.value = request
replyText.value = ''
errorMessage.value = ''
replyModalOpen.value = true
}
const closeReplyModal = () => {
replyModalOpen.value = false
selectedRequest.value = null
replyText.value = ''
errorMessage.value = ''
}
const sendReply = async () => {
if (!selectedRequest.value) return
const text = replyText.value.trim()
if (!text) return
isSendingReply.value = true
errorMessage.value = ''
try {
await $fetch(`/api/cms/contact-requests/${selectedRequest.value.id}/reply`, {
method: 'POST',
body: { message: text }
})
closeReplyModal()
await loadRequests()
if (window.showSuccessModal) {
window.showSuccessModal('Erfolg', 'Antwort wurde erfolgreich versendet.')
}
} catch (error) {
console.error('Fehler beim Senden der Antwort:', error)
errorMessage.value = error?.data?.statusMessage || error?.data?.message || 'Antwort konnte nicht gesendet werden.'
} finally {
isSendingReply.value = false
}
}
onMounted(loadRequests)
definePageMeta({
middleware: 'auth',
layout: 'default'
})
useHead({
title: 'Kontaktanfragen - CMS - Harheimer TC'
})
</script>