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:
@@ -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"
|
||||
|
||||
@@ -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: ''
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
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>
|
||||
@@ -154,7 +154,9 @@ const handleLogin = async () => {
|
||||
// Redirect based on role
|
||||
setTimeout(() => {
|
||||
const roles = response.user.roles || (response.user.role ? [response.user.role] : [])
|
||||
if (roles.includes('admin') || roles.includes('vorstand') || roles.includes('newsletter')) {
|
||||
if (roles.includes('trainer')) {
|
||||
router.push('/cms/kontaktanfragen')
|
||||
} else if (roles.includes('admin') || roles.includes('vorstand') || roles.includes('newsletter')) {
|
||||
router.push('/cms')
|
||||
} else {
|
||||
router.push('/mitgliederbereich')
|
||||
|
||||
Reference in New Issue
Block a user