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:
@@ -202,7 +202,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
<p class="mt-4 text-sm text-gray-600 text-center">
|
<p class="mt-4 text-sm text-gray-600 text-center">
|
||||||
Ihre Nachricht wird direkt an j.dichmann@gmx.de gesendet
|
Ihre Nachricht wird an den Vorstand und die Trainer weitergeleitet
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<span class="text-sm text-gray-500">oder</span>
|
<span class="text-sm text-gray-500">oder</span>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/satzung"
|
to="/verein/satzung"
|
||||||
class="inline-flex items-center px-6 py-3 bg-gray-100 hover:bg-gray-200 text-gray-900 font-semibold rounded-lg transition-colors"
|
class="inline-flex items-center px-6 py-3 bg-gray-100 hover:bg-gray-200 text-gray-900 font-semibold rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Eye
|
<Eye
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<span class="text-sm text-gray-500">oder</span>
|
<span class="text-sm text-gray-500">oder</span>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/satzung"
|
to="/verein/satzung"
|
||||||
class="inline-flex items-center px-6 py-3 bg-gray-100 hover:bg-gray-200 text-gray-900 font-semibold rounded-lg transition-colors"
|
class="inline-flex items-center px-6 py-3 bg-gray-100 hover:bg-gray-200 text-gray-900 font-semibold rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Eye
|
<Eye
|
||||||
|
|||||||
@@ -299,6 +299,16 @@
|
|||||||
Newsletter
|
Newsletter
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="canAccessContactRequests">
|
||||||
|
<div class="h-3 w-px bg-primary-700" />
|
||||||
|
<NuxtLink
|
||||||
|
to="/cms/kontaktanfragen"
|
||||||
|
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||||
|
active-class="text-white bg-primary-600"
|
||||||
|
>
|
||||||
|
Kontaktanfragen
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
<template v-if="isAdmin">
|
<template v-if="isAdmin">
|
||||||
<div class="h-3 w-px bg-primary-700" />
|
<div class="h-3 w-px bg-primary-700" />
|
||||||
<div class="relative inline-block">
|
<div class="relative inline-block">
|
||||||
@@ -371,6 +381,13 @@
|
|||||||
>
|
>
|
||||||
Mitgliederverwaltung
|
Mitgliederverwaltung
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
to="/cms/kontaktanfragen"
|
||||||
|
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors"
|
||||||
|
@click="showCmsDropdown = false"
|
||||||
|
>
|
||||||
|
Kontaktanfragen
|
||||||
|
</NuxtLink>
|
||||||
<div class="border-t border-gray-700 my-1" />
|
<div class="border-t border-gray-700 my-1" />
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/cms/einstellungen"
|
to="/cms/einstellungen"
|
||||||
@@ -707,6 +724,16 @@
|
|||||||
Newsletter
|
Newsletter
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="canAccessContactRequests && !isAdmin">
|
||||||
|
<div class="border-t border-primary-700/20 my-2" />
|
||||||
|
<NuxtLink
|
||||||
|
to="/cms/kontaktanfragen"
|
||||||
|
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"
|
||||||
|
@click="isMobileMenuOpen = false"
|
||||||
|
>
|
||||||
|
Kontaktanfragen
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
<template v-if="isAdmin">
|
<template v-if="isAdmin">
|
||||||
<div class="border-t border-primary-700/20 my-2" />
|
<div class="border-t border-primary-700/20 my-2" />
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
@@ -744,6 +771,13 @@
|
|||||||
>
|
>
|
||||||
Mitgliederverwaltung
|
Mitgliederverwaltung
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
to="/cms/kontaktanfragen"
|
||||||
|
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"
|
||||||
|
@click="isMobileMenuOpen = false"
|
||||||
|
>
|
||||||
|
Kontaktanfragen
|
||||||
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/cms/inhalte"
|
to="/cms/inhalte"
|
||||||
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"
|
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"
|
||||||
@@ -825,6 +859,10 @@ const canAccessNewsletter = computed(() => {
|
|||||||
const store = getAuthStore()
|
const store = getAuthStore()
|
||||||
return store?.hasAnyRole('admin', 'vorstand', 'newsletter') ?? false
|
return store?.hasAnyRole('admin', 'vorstand', 'newsletter') ?? false
|
||||||
})
|
})
|
||||||
|
const canAccessContactRequests = computed(() => {
|
||||||
|
const store = getAuthStore()
|
||||||
|
return store?.hasAnyRole('admin', 'vorstand', 'trainer') ?? false
|
||||||
|
})
|
||||||
|
|
||||||
// Automatisches Setzen des Submenus basierend auf der Route
|
// Automatisches Setzen des Submenus basierend auf der Route
|
||||||
const currentSubmenu = computed(() => {
|
const currentSubmenu = computed(() => {
|
||||||
|
|||||||
@@ -19,12 +19,17 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|||||||
if (to.path.startsWith('/cms')) {
|
if (to.path.startsWith('/cms')) {
|
||||||
const roles = auth.value.roles || (auth.value.role ? [auth.value.role] : [])
|
const roles = auth.value.roles || (auth.value.role ? [auth.value.role] : [])
|
||||||
const hasAccess = roles.includes('admin') || roles.includes('vorstand') || roles.includes('newsletter')
|
const hasAccess = roles.includes('admin') || roles.includes('vorstand') || roles.includes('newsletter')
|
||||||
|
const canAccessContactRequests = roles.includes('admin') || roles.includes('vorstand') || roles.includes('trainer')
|
||||||
|
|
||||||
// Newsletter-Seite nur für Newsletter-Rolle, Admin oder Vorstand
|
// Newsletter-Seite nur für Newsletter-Rolle, Admin oder Vorstand
|
||||||
if (to.path.startsWith('/cms/newsletter')) {
|
if (to.path.startsWith('/cms/newsletter')) {
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
return navigateTo('/mitgliederbereich')
|
return navigateTo('/mitgliederbereich')
|
||||||
}
|
}
|
||||||
|
} else if (to.path.startsWith('/cms/kontaktanfragen')) {
|
||||||
|
if (!canAccessContactRequests) {
|
||||||
|
return navigateTo('/mitgliederbereich')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Andere CMS-Seiten nur für Admin oder Vorstand
|
// Andere CMS-Seiten nur für Admin oder Vorstand
|
||||||
const isAdmin = roles.includes('admin') || roles.includes('vorstand')
|
const isAdmin = roles.includes('admin') || roles.includes('vorstand')
|
||||||
|
|||||||
@@ -70,6 +70,9 @@
|
|||||||
<option value="newsletter">
|
<option value="newsletter">
|
||||||
Newsletter
|
Newsletter
|
||||||
</option>
|
</option>
|
||||||
|
<option value="trainer">
|
||||||
|
Trainer
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- Approve Button -->
|
<!-- Approve Button -->
|
||||||
@@ -177,10 +180,11 @@
|
|||||||
'bg-red-100 text-red-800': role === 'admin',
|
'bg-red-100 text-red-800': role === 'admin',
|
||||||
'bg-blue-100 text-blue-800': role === 'vorstand',
|
'bg-blue-100 text-blue-800': role === 'vorstand',
|
||||||
'bg-green-100 text-green-800': role === 'newsletter',
|
'bg-green-100 text-green-800': role === 'newsletter',
|
||||||
|
'bg-amber-100 text-amber-800': role === 'trainer',
|
||||||
'bg-gray-100 text-gray-800': role === 'mitglied'
|
'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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -280,6 +284,15 @@
|
|||||||
>
|
>
|
||||||
<span class="ml-2 text-sm text-gray-700">Newsletter</span>
|
<span class="ml-2 text-sm text-gray-700">Newsletter</span>
|
||||||
</label>
|
</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">
|
<label class="flex items-center">
|
||||||
<input
|
<input
|
||||||
v-model="selectedRoles"
|
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"
|
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>
|
||||||
|
<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>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Zusatzinfo</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">Zusatzinfo</label>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
@@ -668,6 +676,7 @@ const addTrainer = () => {
|
|||||||
name: '',
|
name: '',
|
||||||
lizenz: '',
|
lizenz: '',
|
||||||
schwerpunkt: '',
|
schwerpunkt: '',
|
||||||
|
email: '',
|
||||||
zusatz: ''
|
zusatz: ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,27 @@
|
|||||||
</p>
|
</p>
|
||||||
</NuxtLink>
|
</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 -->
|
<!-- Startseite -->
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/cms/startseite"
|
to="/cms/startseite"
|
||||||
@@ -179,7 +200,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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'
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
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
|
// Redirect based on role
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const roles = response.user.roles || (response.user.role ? [response.user.role] : [])
|
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')
|
router.push('/cms')
|
||||||
} else {
|
} else {
|
||||||
router.push('/mitgliederbereich')
|
router.push('/mitgliederbereich')
|
||||||
|
|||||||
17
server/api/cms/contact-requests.get.js
Normal file
17
server/api/cms/contact-requests.get.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
||||||
|
import { readContactRequests } from '../../utils/contact-requests.js'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const token = getCookie(event, 'auth_token')
|
||||||
|
const currentUser = token ? await getUserFromToken(token) : null
|
||||||
|
|
||||||
|
if (!currentUser || !hasAnyRole(currentUser, 'admin', 'vorstand', 'trainer')) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Zugriff verweigert'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const requests = await readContactRequests()
|
||||||
|
return requests
|
||||||
|
})
|
||||||
75
server/api/cms/contact-requests/[id]/reply.post.js
Normal file
75
server/api/cms/contact-requests/[id]/reply.post.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
import { getUserFromToken, hasAnyRole } from '../../../../utils/auth.js'
|
||||||
|
import { addContactReply, readContactRequests } from '../../../../utils/contact-requests.js'
|
||||||
|
|
||||||
|
function createTransporter() {
|
||||||
|
const smtpUser = process.env.SMTP_USER
|
||||||
|
const smtpPass = process.env.SMTP_PASS || process.env.EMAIL_PASSWORD
|
||||||
|
if (!smtpUser || !smtpPass) return null
|
||||||
|
|
||||||
|
return nodemailer.createTransport({
|
||||||
|
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
||||||
|
port: Number(process.env.SMTP_PORT || 587),
|
||||||
|
secure: process.env.SMTP_SECURE === 'true',
|
||||||
|
auth: { user: smtpUser, pass: smtpPass }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const token = getCookie(event, 'auth_token')
|
||||||
|
const currentUser = token ? await getUserFromToken(token) : null
|
||||||
|
|
||||||
|
if (!currentUser || !hasAnyRole(currentUser, 'admin', 'vorstand', 'trainer')) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Zugriff verweigert'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody(event)
|
||||||
|
const replyMessage = String(body?.message || '').trim()
|
||||||
|
if (!replyMessage) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Antworttext fehlt' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = getRouterParam(event, 'id')
|
||||||
|
if (!requestId) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Anfrage-ID fehlt' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const all = await readContactRequests()
|
||||||
|
const target = all.find((r) => r.id === requestId)
|
||||||
|
if (!target) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Anfrage nicht gefunden' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const transporter = createTransporter()
|
||||||
|
if (!transporter) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'SMTP ist nicht konfiguriert'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalSubject = target.subject || 'Kontaktanfrage'
|
||||||
|
const responseSubject = `Aw: ${originalSubject}`
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: `"Harheimer TC" <${process.env.SMTP_FROM || process.env.SMTP_USER}>`,
|
||||||
|
to: target.email,
|
||||||
|
subject: responseSubject,
|
||||||
|
text: replyMessage
|
||||||
|
})
|
||||||
|
|
||||||
|
const responderEmail = currentUser.email || ''
|
||||||
|
const updated = await addContactReply({
|
||||||
|
requestId,
|
||||||
|
replyText: replyMessage,
|
||||||
|
responderEmail
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
request: updated
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
const { userId, roles } = body
|
const { userId, roles } = body
|
||||||
|
|
||||||
const validRoles = ['mitglied', 'vorstand', 'admin', 'newsletter']
|
const validRoles = ['mitglied', 'vorstand', 'admin', 'newsletter', 'trainer']
|
||||||
const rolesArray = Array.isArray(roles) ? roles : (roles ? [roles] : ['mitglied'])
|
const rolesArray = Array.isArray(roles) ? roles : (roles ? [roles] : ['mitglied'])
|
||||||
|
|
||||||
if (!rolesArray.every(r => validRoles.includes(r))) {
|
if (!rolesArray.every(r => validRoles.includes(r))) {
|
||||||
|
|||||||
@@ -1,10 +1,93 @@
|
|||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
|
import { promises as fs } from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { createContactRequest } from '../utils/contact-requests.js'
|
||||||
|
import { readUsers, migrateUserRoles } from '../utils/auth.js'
|
||||||
|
|
||||||
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
||||||
|
// filename is always a hardcoded constant ('config.json'), never user input
|
||||||
|
const getDataPath = (filename) => {
|
||||||
|
const cwd = process.cwd()
|
||||||
|
if (cwd.endsWith('.output')) return path.join(cwd, '../server/data', filename)
|
||||||
|
return path.join(cwd, 'server/data', filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const configFile = getDataPath('config.json')
|
||||||
|
const raw = await fs.readFile(configFile, 'utf-8')
|
||||||
|
return JSON.parse(raw)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Konfiguration für Kontaktanfragen:', error)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectRecipients(config) {
|
||||||
|
const recipients = []
|
||||||
|
|
||||||
|
// Vorstand
|
||||||
|
if (config?.vorstand && typeof config.vorstand === 'object') {
|
||||||
|
for (const member of Object.values(config.vorstand)) {
|
||||||
|
if (member?.email && typeof member.email === 'string' && member.email.trim()) {
|
||||||
|
recipients.push(member.email.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trainer
|
||||||
|
if (Array.isArray(config?.trainer)) {
|
||||||
|
for (const trainer of config.trainer) {
|
||||||
|
if (trainer?.email && typeof trainer.email === 'string' && trainer.email.trim()) {
|
||||||
|
recipients.push(trainer.email.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zusätzlich: Benutzer mit Trainer-Rolle aus dem Login-System
|
||||||
|
try {
|
||||||
|
const users = await readUsers()
|
||||||
|
for (const rawUser of users) {
|
||||||
|
const user = migrateUserRoles({ ...rawUser })
|
||||||
|
const roles = Array.isArray(user.roles) ? user.roles : []
|
||||||
|
if (roles.includes('trainer') && user.email && String(user.email).trim()) {
|
||||||
|
recipients.push(String(user.email).trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Trainer-Empfänger aus Benutzerdaten:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const unique = [...new Set(recipients)]
|
||||||
|
if (unique.length > 0) return unique
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
if (config?.website?.verantwortlicher?.email) {
|
||||||
|
return [config.website.verantwortlicher.email]
|
||||||
|
}
|
||||||
|
if (process.env.SMTP_USER) {
|
||||||
|
return [process.env.SMTP_USER]
|
||||||
|
}
|
||||||
|
return ['j.dichmann@gmx.de']
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTransporter() {
|
||||||
|
const smtpUser = process.env.SMTP_USER
|
||||||
|
const smtpPass = process.env.SMTP_PASS || process.env.EMAIL_PASSWORD
|
||||||
|
if (!smtpUser || !smtpPass) return null
|
||||||
|
|
||||||
|
return nodemailer.createTransport({
|
||||||
|
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
||||||
|
port: Number(process.env.SMTP_PORT || 587),
|
||||||
|
secure: process.env.SMTP_SECURE === 'true',
|
||||||
|
auth: { user: smtpUser, pass: smtpPass }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
|
|
||||||
// Validierung der Eingabedaten
|
|
||||||
if (!body.name || !body.email || !body.subject || !body.message) {
|
if (!body.name || !body.email || !body.subject || !body.message) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
@@ -12,7 +95,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// E-Mail-Validierung
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
if (!emailRegex.test(body.email)) {
|
if (!emailRegex.test(body.email)) {
|
||||||
throw createError({
|
throw createError({
|
||||||
@@ -21,34 +103,32 @@ export default defineEventHandler(async (event) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SMTP-Konfiguration (hier können Sie Ihre SMTP-Daten eintragen)
|
// Anfrage immer speichern, auch wenn E-Mail-Versand fehlschlägt.
|
||||||
const smtpUser = process.env.SMTP_USER || 'j.dichmann@gmx.de'
|
await createContactRequest({
|
||||||
const smtpPass = process.env.SMTP_PASS || process.env.EMAIL_PASSWORD
|
name: String(body.name).trim(),
|
||||||
|
email: String(body.email).trim(),
|
||||||
if (!smtpUser || !smtpPass) {
|
phone: body.phone ? String(body.phone).trim() : '',
|
||||||
throw createError({
|
subject: String(body.subject).trim(),
|
||||||
statusCode: 500,
|
message: String(body.message).trim()
|
||||||
statusMessage: 'SMTP-Credentials fehlen! Bitte setzen Sie SMTP_USER und SMTP_PASS in der .env Datei.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
|
||||||
port: process.env.SMTP_PORT || 587,
|
|
||||||
secure: false, // true für 465, false für andere Ports
|
|
||||||
auth: {
|
|
||||||
user: smtpUser,
|
|
||||||
pass: smtpPass
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// E-Mail-Template
|
const config = await loadConfig()
|
||||||
|
const recipients = await collectRecipients(config)
|
||||||
|
const transporter = createTransporter()
|
||||||
|
|
||||||
|
if (!transporter) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Anfrage wurde gespeichert. E-Mail-Versand ist aktuell nicht konfiguriert.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nowLabel = new Date().toLocaleString('de-DE')
|
||||||
const emailHtml = `
|
const emailHtml = `
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||||
<h2 style="color: #dc2626; border-bottom: 2px solid #dc2626; padding-bottom: 10px;">
|
<h2 style="color: #dc2626; border-bottom: 2px solid #dc2626; padding-bottom: 10px;">
|
||||||
Neue Kontaktanfrage - Harheimer TC
|
Neue Kontaktanfrage - Harheimer TC
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div style="background-color: #f9fafb; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
<div style="background-color: #f9fafb; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||||
<h3 style="color: #374151; margin-top: 0;">Kontaktdaten:</h3>
|
<h3 style="color: #374151; margin-top: 0;">Kontaktdaten:</h3>
|
||||||
<p><strong>Name:</strong> ${body.name}</p>
|
<p><strong>Name:</strong> ${body.name}</p>
|
||||||
@@ -56,21 +136,18 @@ export default defineEventHandler(async (event) => {
|
|||||||
<p><strong>Telefon:</strong> ${body.phone || 'Nicht angegeben'}</p>
|
<p><strong>Telefon:</strong> ${body.phone || 'Nicht angegeben'}</p>
|
||||||
<p><strong>Betreff:</strong> ${body.subject}</p>
|
<p><strong>Betreff:</strong> ${body.subject}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="background-color: #ffffff; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;">
|
<div style="background-color: #ffffff; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;">
|
||||||
<h3 style="color: #374151; margin-top: 0;">Nachricht:</h3>
|
<h3 style="color: #374151; margin-top: 0;">Nachricht:</h3>
|
||||||
<p style="white-space: pre-wrap; line-height: 1.6;">${body.message}</p>
|
<p style="white-space: pre-wrap; line-height: 1.6;">${body.message}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px;">
|
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px;">
|
||||||
<p>Diese Nachricht wurde über das Kontaktformular der Harheimer TC Website gesendet.</p>
|
<p>Diese Nachricht wurde über das Kontaktformular der Harheimer TC Website gesendet.</p>
|
||||||
<p>Zeitstempel: ${new Date().toLocaleString('de-DE')}</p>
|
<p>Zeitstempel: ${nowLabel}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
const emailText = `
|
const emailText = `Neue Kontaktanfrage - Harheimer TC
|
||||||
Neue Kontaktanfrage - Harheimer TC
|
|
||||||
|
|
||||||
Kontaktdaten:
|
Kontaktdaten:
|
||||||
Name: ${body.name}
|
Name: ${body.name}
|
||||||
@@ -83,36 +160,29 @@ ${body.message}
|
|||||||
|
|
||||||
---
|
---
|
||||||
Diese Nachricht wurde über das Kontaktformular der Harheimer TC Website gesendet.
|
Diese Nachricht wurde über das Kontaktformular der Harheimer TC Website gesendet.
|
||||||
Zeitstempel: ${new Date().toLocaleString('de-DE')}
|
Zeitstempel: ${nowLabel}`
|
||||||
`
|
|
||||||
|
|
||||||
// E-Mail senden
|
await transporter.sendMail({
|
||||||
const mailOptions = {
|
from: `"Harheimer TC Website" <${process.env.SMTP_FROM || process.env.SMTP_USER}>`,
|
||||||
from: `"Harheimer TC Website" <${process.env.SMTP_USER || 'j.dichmann@gmx.de'}>`,
|
to: recipients.join(', '),
|
||||||
to: 'j.dichmann@gmx.de',
|
|
||||||
replyTo: body.email,
|
replyTo: body.email,
|
||||||
subject: `Kontaktanfrage: ${body.subject}`,
|
subject: `Kontaktanfrage: ${body.subject}`,
|
||||||
text: emailText,
|
text: emailText,
|
||||||
html: emailHtml
|
html: emailHtml
|
||||||
}
|
})
|
||||||
|
|
||||||
await transporter.sendMail(mailOptions)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: 'E-Mail wurde erfolgreich gesendet!'
|
message: 'Anfrage wurde erfolgreich gesendet.'
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Senden der E-Mail:', error)
|
console.error('Fehler bei Kontaktanfrage:', error)
|
||||||
|
|
||||||
if (error.statusCode) {
|
if (error.statusCode) throw error
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
statusMessage: 'Fehler beim Senden der E-Mail. Bitte versuchen Sie es später erneut.'
|
statusMessage: 'Fehler beim Senden der Anfrage. Bitte versuchen Sie es später erneut.'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
79
server/utils/contact-requests.js
Normal file
79
server/utils/contact-requests.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { promises as fs } from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
|
||||||
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
||||||
|
// filename is always a hardcoded constant, never user input
|
||||||
|
const getDataPath = (filename) => {
|
||||||
|
const cwd = process.cwd()
|
||||||
|
if (cwd.endsWith('.output')) {
|
||||||
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
||||||
|
return path.join(cwd, '../server/data', filename)
|
||||||
|
}
|
||||||
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
||||||
|
return path.join(cwd, 'server/data', filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONTACT_REQUESTS_FILE = getDataPath('contact-requests.json')
|
||||||
|
|
||||||
|
export async function readContactRequests() {
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(CONTACT_REQUESTS_FILE, 'utf-8')
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
return Array.isArray(parsed) ? parsed : []
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') return []
|
||||||
|
console.error('Fehler beim Lesen der Kontaktanfragen:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeContactRequests(items) {
|
||||||
|
await fs.writeFile(CONTACT_REQUESTS_FILE, JSON.stringify(items, null, 2), 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createContactRequest(data) {
|
||||||
|
const current = await readContactRequests()
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
const item = {
|
||||||
|
id: randomUUID(),
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
status: 'offen',
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
phone: data.phone || '',
|
||||||
|
subject: data.subject,
|
||||||
|
message: data.message,
|
||||||
|
replies: []
|
||||||
|
}
|
||||||
|
current.unshift(item)
|
||||||
|
await writeContactRequests(current)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addContactReply({ requestId, replyText, responderEmail }) {
|
||||||
|
const current = await readContactRequests()
|
||||||
|
const index = current.findIndex((r) => r.id === requestId)
|
||||||
|
if (index === -1) return null
|
||||||
|
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
const request = current[index]
|
||||||
|
const replies = Array.isArray(request.replies) ? request.replies : []
|
||||||
|
replies.push({
|
||||||
|
id: randomUUID(),
|
||||||
|
createdAt: now,
|
||||||
|
responderEmail: responderEmail || '',
|
||||||
|
message: replyText
|
||||||
|
})
|
||||||
|
|
||||||
|
current[index] = {
|
||||||
|
...request,
|
||||||
|
status: 'beantwortet',
|
||||||
|
replies,
|
||||||
|
updatedAt: now
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeContactRequests(current)
|
||||||
|
return current[index]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user