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
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 56s
This commit is contained in:
229
pages/cms/kontaktanfragen.vue
Normal file
229
pages/cms/kontaktanfragen.vue
Normal 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>
|
||||
Reference in New Issue
Block a user