Update dependencies to include TinyMCE and Quill, enhance Navigation component with a new Newsletter submenu, and implement role-based access control for CMS features. Refactor user role handling to support multiple roles and improve user management functionality across various API endpoints.

This commit is contained in:
Torsten Schulz (local)
2025-12-19 09:51:28 +01:00
parent baf6c59c0d
commit 435e28fd55
69 changed files with 5034 additions and 276 deletions

View File

@@ -46,6 +46,7 @@
<option value="mitglied">Mitglied</option>
<option value="vorstand">Vorstand</option>
<option value="admin">Administrator</option>
<option value="newsletter">Newsletter</option>
</select>
<!-- Approve Button -->
@@ -112,20 +113,27 @@
<div class="text-sm text-gray-600">{{ user.phone || '-' }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<select
v-model="user.role"
@change="updateUserRole(user)"
class="px-3 py-1 border border-gray-300 rounded text-sm"
:class="{
'bg-red-50 border-red-300': user.role === 'admin',
'bg-blue-50 border-blue-300': user.role === 'vorstand',
'bg-gray-50 border-gray-300': user.role === 'mitglied'
}"
<div class="flex flex-wrap gap-1">
<span
v-for="role in (user.roles || (user.role ? [user.role] : ['mitglied']))"
:key="role"
class="px-2 py-1 text-xs font-medium rounded"
:class="{
'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-gray-100 text-gray-800': role === 'mitglied'
}"
>
{{ role === 'admin' ? 'Admin' : role === 'vorstand' ? 'Vorstand' : role === 'newsletter' ? 'Newsletter' : 'Mitglied' }}
</span>
</div>
<button
@click="openRoleModal(user)"
class="mt-1 text-xs text-primary-600 hover:text-primary-800"
>
<option value="mitglied">Mitglied</option>
<option value="vorstand">Vorstand</option>
<option value="admin">Administrator</option>
</select>
Bearbeiten
</button>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-600">
@@ -162,6 +170,79 @@
</p>
</div>
</div>
<!-- Role Edit Modal -->
<div
v-if="showRoleModal && editingUser"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click.self="closeRoleModal"
>
<div class="bg-white rounded-xl shadow-2xl max-w-md w-full p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-4">
Rollen bearbeiten: {{ editingUser.name }}
</h2>
<div class="space-y-3 mb-6">
<label class="flex items-center">
<input
type="checkbox"
v-model="selectedRoles"
value="mitglied"
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">Mitglied</span>
</label>
<label class="flex items-center">
<input
type="checkbox"
v-model="selectedRoles"
value="vorstand"
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">Vorstand</span>
</label>
<label class="flex items-center">
<input
type="checkbox"
v-model="selectedRoles"
value="newsletter"
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">Newsletter</span>
</label>
<label class="flex items-center">
<input
type="checkbox"
v-model="selectedRoles"
value="admin"
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">Administrator</span>
</label>
</div>
<div v-if="selectedRoles.length === 0" class="mb-4 text-sm text-red-600">
Mindestens eine Rolle muss ausgewählt werden.
</div>
<div class="flex justify-end space-x-3">
<button
type="button"
@click="closeRoleModal"
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
Abbrechen
</button>
<button
@click="saveUserRoles"
:disabled="selectedRoles.length === 0"
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Speichern
</button>
</div>
</div>
</div>
</div>
</template>
@@ -173,11 +254,17 @@ const allUsers = ref([])
const currentUserId = ref(null)
const successMessage = ref('')
const errorMessage = ref('')
const showRoleModal = ref(false)
const editingUser = ref(null)
const selectedRoles = ref([])
const pendingUsers = computed(() => {
return allUsers.value
.filter(u => u.active === false)
.map(u => ({ ...u, selectedRole: u.role || 'mitglied' }))
.map(u => ({
...u,
selectedRole: (u.roles && u.roles.length > 0) ? u.roles[0] : (u.role || 'mitglied')
}))
})
const activeUsers = computed(() => {
@@ -210,7 +297,7 @@ const approveUser = async (user) => {
method: 'POST',
body: {
userId: user.id,
role: user.selectedRole
roles: [user.selectedRole || 'mitglied']
}
})
@@ -224,6 +311,41 @@ const approveUser = async (user) => {
}
}
function openRoleModal(user) {
editingUser.value = user
selectedRoles.value = user.roles || (user.role ? [user.role] : ['mitglied'])
showRoleModal.value = true
}
function closeRoleModal() {
showRoleModal.value = false
editingUser.value = null
selectedRoles.value = []
}
async function saveUserRoles() {
if (!editingUser.value || selectedRoles.value.length === 0) return
try {
await $fetch('/api/cms/users/update-role', {
method: 'POST',
body: {
userId: editingUser.value.id,
roles: selectedRoles.value
}
})
successMessage.value = `Rollen von ${editingUser.value.name} wurden aktualisiert`
setTimeout(() => successMessage.value = '', 3000)
closeRoleModal()
await loadUsers()
} catch (error) {
errorMessage.value = 'Fehler beim Aktualisieren der Rollen'
setTimeout(() => errorMessage.value = '', 3000)
}
}
const rejectUser = async (user) => {
window.showConfirmModal('Registrierung ablehnen', `Möchten Sie die Registrierung von ${user.name} wirklich ablehnen?`, async () => {
try {
@@ -241,24 +363,6 @@ const rejectUser = async (user) => {
})
}
const updateUserRole = async (user) => {
try {
await $fetch('/api/cms/users/update-role', {
method: 'POST',
body: {
userId: user.id,
role: user.role
}
})
successMessage.value = `Rolle von ${user.name} wurde aktualisiert`
setTimeout(() => successMessage.value = '', 3000)
} catch (error) {
errorMessage.value = 'Fehler beim Aktualisieren der Rolle'
setTimeout(() => errorMessage.value = '', 3000)
await loadUsers() // Reload to revert changes
}
}
const deactivateUser = async (user) => {
window.showConfirmModal('Benutzer deaktivieren', `Möchten Sie ${user.name} wirklich deaktivieren?`, async () => {

View File

@@ -138,7 +138,7 @@
<!-- Benutzerverwaltung (nur für Admin) -->
<NuxtLink
v-if="authStore.role === 'admin'"
v-if="authStore.hasRole('admin')"
to="/cms/benutzer"
class="bg-white p-6 rounded-xl shadow-lg border border-gray-100 hover:shadow-xl transition-all group"
>

884
pages/cms/newsletter.vue Normal file
View File

@@ -0,0 +1,884 @@
<template>
<div class="min-h-full bg-gray-50">
<!-- Fixed Header -->
<div class="fixed top-20 left-0 right-0 z-40 bg-white border-b border-gray-200 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 sm:py-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl sm:text-4xl font-display font-bold text-gray-900">
Newsletter
</h1>
<div class="w-16 sm:w-24 h-1 bg-primary-600 mt-1 sm:mt-2" />
</div>
<div class="space-x-3">
<button
v-if="canCreateGroup"
@click="showCreateGroupModal = true"
class="inline-flex items-center px-3 py-1.5 sm:px-4 sm:py-2 rounded-lg bg-primary-600 text-white hover:bg-primary-700 text-sm sm:text-base"
>
<Plus :size="16" class="mr-2" />
Neue Newsletter-Gruppe
</button>
</div>
</div>
</div>
</div>
<!-- Content -->
<div class="pt-28 sm:pt-32 pb-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center py-12">
<Loader2 :size="40" class="animate-spin text-primary-600" />
</div>
<!-- Newsletter Groups List -->
<div v-else class="space-y-6">
<div
v-for="group in groups"
:key="group.id"
class="bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden"
>
<!-- Group Header -->
<div class="p-6 border-b border-gray-200">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center space-x-3 mb-2">
<h3 class="text-xl font-semibold text-gray-900">{{ group.name }}</h3>
<span
class="px-2 py-1 text-xs font-medium rounded bg-blue-100 text-blue-800"
>
{{ group.type === 'subscription' ? 'Abonnenten' : 'Gruppe' }}
</span>
</div>
<p v-if="group.description" class="text-sm text-gray-600 mb-2">
{{ group.description }}
</p>
<div class="flex items-center space-x-4 text-sm text-gray-500">
<span>Erstellt: {{ formatDate(group.createdAt) }}</span>
<span>{{ group.postCount || 0 }} Posts</span>
<span v-if="group.type === 'group'">
Zielgruppe: {{ formatTargetGroup(group.targetGroup) }}
</span>
<span v-if="group.type === 'subscription'">
{{ group.sendToExternal ? 'Intern & Extern' : 'Nur Intern' }}
</span>
</div>
</div>
<div class="flex items-center space-x-2 ml-4">
<button
v-if="group.type === 'subscription'"
@click="showSubscribersModal(group)"
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm"
>
<Users :size="16" class="inline mr-1" />
Abonnenten
</button>
<button
@click="showPostModal(group)"
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm"
>
<Plus :size="16" class="inline mr-1" />
Post hinzufügen
</button>
</div>
</div>
</div>
<!-- Posts List Header -->
<div v-if="groupPosts[group.id] && groupPosts[group.id].length > 0" class="border-t border-gray-200">
<button
@click="toggleGroupPosts(group.id)"
class="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition-colors"
>
<span class="text-sm font-medium text-gray-700">
Posts ({{ groupPosts[group.id].length }})
</span>
<svg
:class="['w-5 h-5 text-gray-500 transition-transform', expandedGroups[group.id] ? 'rotate-180' : '']"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<!-- Collapsible Posts List -->
<div v-show="expandedGroups[group.id]" class="divide-y divide-gray-200">
<div
v-for="post in groupPosts[group.id]"
:key="post.id"
class="p-6 hover:bg-gray-50 transition-colors"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<h4 class="text-lg font-semibold text-gray-900 mb-2">{{ post.title }}</h4>
<div class="flex items-center space-x-4 text-sm text-gray-500 mb-3">
<span v-if="post.sentAt">Versendet: {{ formatDate(post.sentAt) }}</span>
<span v-else class="text-yellow-600">Nicht versendet</span>
<span v-if="post.sentTo && post.sentTo.total > 0">
Empfänger: {{ post.sentTo.sent }}/{{ post.sentTo.total }}
</span>
<span v-else-if="post.sentTo && post.sentTo.total === 0" class="text-gray-400">
Keine Empfänger gefunden
</span>
</div>
<div
v-html="post.content.substring(0, 200) + (post.content.length > 200 ? '...' : '')"
class="text-sm text-gray-600 prose prose-sm max-w-none mb-3"
></div>
<!-- Empfängerliste (collapsible) -->
<div v-if="post.sentTo && post.sentTo.recipients && post.sentTo.recipients.length > 0" class="border-t border-gray-200 mt-3 pt-3">
<button
@click="togglePostRecipients(post.id)"
class="w-full flex items-center justify-between text-sm text-gray-600 hover:text-gray-900 transition-colors"
>
<span class="font-medium">
Empfänger ({{ post.sentTo.recipients.length }})
</span>
<svg
:class="['w-4 h-4 transition-transform', expandedPosts[post.id] ? 'rotate-180' : '']"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div v-show="expandedPosts[post.id]" class="mt-3 space-y-2">
<div
v-for="(recipient, idx) in post.sentTo.recipients"
:key="idx"
class="flex items-center justify-between text-sm py-1 px-2 rounded"
:class="recipient.sent ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'"
>
<div>
<span class="font-medium">{{ recipient.email }}</span>
<span v-if="recipient.name" class="text-gray-600 ml-2">({{ recipient.name }})</span>
</div>
<span class="text-xs">
{{ recipient.sent ? '✓ Versendet' : '✗ Fehler' }}
</span>
</div>
</div>
</div>
<div v-else-if="post.sentTo && post.sentTo.total === 0" class="border-t border-gray-200 mt-3 pt-3 text-sm text-gray-500">
Keine Empfänger gefunden
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="p-6 text-center text-gray-500 text-sm border-t border-gray-200">
Noch keine Posts in dieser Gruppe
</div>
</div>
<div v-if="groups.length === 0" class="text-center py-12 text-gray-500">
Noch keine Newsletter-Gruppen vorhanden.
</div>
</div>
</div>
</div>
<!-- Create Group Modal -->
<div
v-if="showCreateGroupModal"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click.self="closeGroupModal"
>
<div class="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] flex flex-col">
<div class="p-6 border-b border-gray-200 flex-shrink-0">
<h3 class="text-lg font-semibold text-gray-900">
Neue Newsletter-Gruppe erstellen
</h3>
</div>
<div class="overflow-y-auto flex-1 p-6">
<form id="group-form" @submit.prevent="saveGroup" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Name *
</label>
<input
v-model="groupFormData.name"
type="text"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="z.B. Allgemeiner Newsletter"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Beschreibung (optional)
</label>
<textarea
v-model="groupFormData.description"
rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Beschreibung der Newsletter-Gruppe"
></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Typ *
</label>
<select
v-model="groupFormData.type"
required
@change="onGroupTypeChange"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="">Bitte wählen</option>
<option value="subscription">Abonnenten-Newsletter</option>
<option value="group">Gruppen-Newsletter</option>
</select>
</div>
<div v-if="groupFormData.type === 'subscription'">
<label class="block text-sm font-medium text-gray-700 mb-2">
Empfänger
</label>
<select
v-model="groupFormData.sendToExternal"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option :value="false">Nur Intern</option>
<option :value="true">Auch Extern</option>
</select>
</div>
<div v-if="groupFormData.type === 'group'">
<label class="block text-sm font-medium text-gray-700 mb-2">
Zielgruppe *
</label>
<select
v-model="groupFormData.targetGroup"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="">Bitte wählen</option>
<option value="alle">Alle</option>
<option value="erwachsene">Erwachsene</option>
<option value="nachwuchs">Nachwuchs</option>
<option value="mannschaftsspieler">Mannschaftsspieler</option>
<option value="vorstand">Vorstand</option>
</select>
</div>
</form>
</div>
<div class="p-6 border-t border-gray-200 flex justify-end space-x-3 flex-shrink-0">
<button
type="button"
@click="closeGroupModal"
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
Abbrechen
</button>
<button
type="submit"
form="group-form"
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
Erstellen
</button>
</div>
</div>
</div>
<!-- Create Post Modal -->
<div
v-if="showPostModalForGroup"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click.self="closePostModal"
>
<div class="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] flex flex-col">
<div class="p-6 border-b border-gray-200 flex-shrink-0">
<h3 class="text-lg font-semibold text-gray-900">
Post zu "{{ showPostModalForGroup.name }}" hinzufügen
</h3>
<p class="text-sm text-gray-500 mt-1">
Der Post wird automatisch an alle Abonnenten dieser Gruppe versendet.
</p>
</div>
<div class="overflow-y-auto flex-1 p-6">
<form id="post-form" @submit.prevent="savePost" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Titel *
</label>
<input
v-model="postFormData.title"
type="text"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Post-Titel"
/>
</div>
<div>
<RichTextEditor
v-model="postFormData.content"
label="Inhalt *"
:required="true"
/>
</div>
</form>
</div>
<div class="p-6 border-t border-gray-200 flex-shrink-0">
<!-- Erfolgsmeldung -->
<div v-if="postSuccessMessage" class="space-y-4">
<div class="p-4 bg-green-50 border border-green-200 rounded-lg">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3 flex-1">
<p class="text-sm font-medium text-green-800">
{{ postSuccessMessage }}
</p>
<div v-if="postSuccessStats" class="mt-2 text-sm text-green-700">
<p>Empfänger: {{ postSuccessStats.sent }}/{{ postSuccessStats.total }} erfolgreich versendet</p>
<div v-if="postSuccessStats.failed > 0" class="mt-2">
<p class="font-medium"> {{ postSuccessStats.failed }} Fehler beim Versenden:</p>
<ul v-if="postSuccessStats.errorDetails" class="list-disc list-inside mt-1 space-y-1">
<li v-for="err in postSuccessStats.errorDetails" :key="err.email">
{{ err.email }}: {{ err.error }}
</li>
</ul>
<p v-else-if="postSuccessStats.failedEmails" class="mt-1">
{{ postSuccessStats.failedEmails.join(', ') }}
</p>
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-end">
<button
@click="closePostModal"
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
Schließen
</button>
</div>
</div>
<!-- Formular-Buttons -->
<div v-else class="flex justify-end space-x-3">
<button
type="button"
@click="closePostModal"
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
Abbrechen
</button>
<button
type="submit"
form="post-form"
:disabled="isSendingPost"
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{{ isSendingPost ? 'Wird versendet...' : 'Erstellen & Versenden' }}
</button>
</div>
</div>
</div>
</div>
<!-- Subscribers Modal -->
<div
v-if="showSubscribersModalForGroup"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click.self="closeSubscribersModal"
>
<div class="bg-white rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] flex flex-col">
<div class="p-6 border-b border-gray-200 flex-shrink-0">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-display font-bold text-gray-900">
Abonnenten: {{ showSubscribersModalForGroup.name }}
</h2>
<button
@click="showAddSubscriberModal = true"
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm flex items-center"
>
<Plus :size="16" class="mr-2" />
Empfänger hinzufügen
</button>
</div>
</div>
<div class="overflow-y-auto flex-1 p-6">
<div v-if="isLoadingSubscribers" class="flex items-center justify-center py-12">
<Loader2 :size="40" class="animate-spin text-primary-600" />
</div>
<div v-else-if="subscribers.length === 0" class="text-center py-12 text-gray-500">
Keine Abonnenten gefunden.
</div>
<div v-else class="space-y-4">
<div class="bg-gray-50 rounded-lg p-4 mb-4">
<p class="text-sm text-gray-600">
<strong>{{ subscribers.length }}</strong> Abonnent{{ subscribers.length !== 1 ? 'en' : '' }}
</p>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
E-Mail
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Angemeldet
</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Aktionen
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="subscriber in subscribers" :key="subscriber.id" class="hover:bg-gray-50">
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
{{ subscriber.email }}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
{{ subscriber.name || '-' }}
</td>
<td class="px-4 py-3 whitespace-nowrap">
<span
:class="[
'px-2 py-1 text-xs font-medium rounded-full',
subscriber.confirmed && !subscriber.unsubscribedAt
? 'bg-green-100 text-green-800'
: subscriber.unsubscribedAt
? 'bg-red-100 text-red-800'
: 'bg-yellow-100 text-yellow-800'
]"
>
{{
subscriber.confirmed && !subscriber.unsubscribedAt
? 'Bestätigt'
: subscriber.unsubscribedAt
? 'Abgemeldet'
: 'Ausstehend'
}}
</span>
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
{{ formatDate(subscriber.subscribedAt) }}
</td>
<td class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
<button
@click="removeSubscriber(subscriber.id)"
class="text-red-600 hover:text-red-900"
title="Abonnent entfernen"
>
<Trash2 :size="18" />
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="p-6 border-t border-gray-200 flex justify-end flex-shrink-0">
<button
type="button"
@click="closeSubscribersModal"
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
Schließen
</button>
</div>
</div>
</div>
<!-- Add Subscriber Modal -->
<div
v-if="showAddSubscriberModal"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click.self="closeAddSubscriberModal"
>
<div class="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] flex flex-col">
<div class="p-6 border-b border-gray-200 flex-shrink-0">
<h2 class="text-2xl font-display font-bold text-gray-900">
Empfänger hinzufügen: {{ showSubscribersModalForGroup?.name }}
</h2>
<p class="text-sm text-gray-500 mt-1">
Der Empfänger erhält eine Bestätigungsmail mit Ihrer individuellen Nachricht.
</p>
</div>
<div class="overflow-y-auto flex-1 p-6">
<form id="add-subscriber-form" @submit.prevent="addSubscriber" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
E-Mail-Adresse *
</label>
<input
v-model="addSubscriberForm.email"
type="email"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="empfaenger@example.com"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Name (optional)
</label>
<input
v-model="addSubscriberForm.name"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Name des Empfängers"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Individuelle Nachricht (optional)
</label>
<textarea
v-model="addSubscriberForm.customMessage"
rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Diese Nachricht wird in der Bestätigungsmail angezeigt..."
></textarea>
<p class="text-xs text-gray-500 mt-1">
Diese Nachricht wird in der Bestätigungsmail angezeigt, um den Empfänger persönlich anzusprechen.
</p>
</div>
<div v-if="addSubscriberError" class="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{{ addSubscriberError }}
</div>
<div v-if="addSubscriberSuccess" class="p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">
{{ addSubscriberSuccess }}
</div>
</form>
</div>
<div class="p-6 border-t border-gray-200 flex justify-end space-x-3 flex-shrink-0">
<button
type="button"
@click="closeAddSubscriberModal"
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
:disabled="isAddingSubscriber"
>
Abbrechen
</button>
<button
type="submit"
form="add-subscriber-form"
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors flex items-center disabled:opacity-50"
:disabled="isAddingSubscriber"
>
<Loader2 v-if="isAddingSubscriber" :size="16" class="animate-spin mr-2" />
<span>{{ isAddingSubscriber ? 'Wird hinzugefügt...' : 'Hinzufügen' }}</span>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Plus, Loader2, Users, Trash2 } from 'lucide-vue-next'
import RichTextEditor from '~/components/RichTextEditor.vue'
useHead({
title: 'Newsletter-Verwaltung - CMS - Harheimer TC',
})
const groups = ref([])
const groupPosts = ref({})
const expandedGroups = ref({}) // Track which groups have expanded posts
const expandedPosts = ref({}) // Track which posts have expanded recipients
const isLoading = ref(true)
const showCreateGroupModal = ref(false)
const showPostModalForGroup = ref(null)
const isSendingPost = ref(false)
const postSuccessMessage = ref(null)
const postSuccessStats = ref(null)
const showSubscribersModalForGroup = ref(null)
const subscribers = ref([])
const isLoadingSubscribers = ref(false)
const showAddSubscriberModal = ref(false)
const addSubscriberForm = ref({
email: '',
name: '',
customMessage: ''
})
const isAddingSubscriber = ref(false)
const addSubscriberError = ref('')
const addSubscriberSuccess = ref('')
const groupFormData = ref({
name: '',
description: '',
type: '',
targetGroup: '',
sendToExternal: false
})
const postFormData = ref({
title: '',
content: ''
})
onMounted(async () => {
await loadGroups()
})
async function loadGroups() {
try {
isLoading.value = true
const response = await $fetch('/api/newsletter/groups/list')
groups.value = response.groups || []
// Lade Posts für jede Gruppe
for (const group of groups.value) {
await loadPostsForGroup(group.id)
}
} catch (error) {
console.error('Fehler beim Laden der Newsletter-Gruppen:', error)
} finally {
isLoading.value = false
}
}
async function loadPostsForGroup(groupId) {
try {
const response = await $fetch(`/api/newsletter/groups/${groupId}/posts/list`)
groupPosts.value[groupId] = response.posts || []
} catch (error) {
console.error(`Fehler beim Laden der Posts für Gruppe ${groupId}:`, error)
groupPosts.value[groupId] = []
}
}
function formatDate(dateString) {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleDateString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
function formatTargetGroup(group) {
const groups = {
alle: 'Alle',
erwachsene: 'Erwachsene',
nachwuchs: 'Nachwuchs',
mannschaftsspieler: 'Mannschaftsspieler',
vorstand: 'Vorstand'
}
return groups[group] || group
}
function toggleGroupPosts(groupId) {
expandedGroups.value[groupId] = !expandedGroups.value[groupId]
}
function togglePostRecipients(postId) {
expandedPosts.value[postId] = !expandedPosts.value[postId]
}
function onGroupTypeChange() {
if (groupFormData.value.type === 'subscription') {
groupFormData.value.targetGroup = ''
} else if (groupFormData.value.type === 'group') {
groupFormData.value.sendToExternal = false
}
}
function closeGroupModal() {
showCreateGroupModal.value = false
groupFormData.value = {
name: '',
description: '',
type: '',
targetGroup: '',
sendToExternal: false
}
}
async function saveGroup() {
try {
await $fetch('/api/newsletter/groups/create', {
method: 'POST',
body: groupFormData.value
})
await loadGroups()
closeGroupModal()
} catch (error) {
console.error('Fehler beim Erstellen der Newsletter-Gruppe:', error)
alert(error.data?.statusMessage || 'Fehler beim Erstellen der Newsletter-Gruppe')
}
}
function showPostModal(group) {
showPostModalForGroup.value = group
postFormData.value = {
title: '',
content: ''
}
postSuccessMessage.value = null
postSuccessStats.value = null
}
function closePostModal() {
showPostModalForGroup.value = null
postFormData.value = {
title: '',
content: ''
}
postSuccessMessage.value = null
postSuccessStats.value = null
}
async function showSubscribersModal(group) {
showSubscribersModalForGroup.value = group
await loadSubscribers(group.id)
}
function closeSubscribersModal() {
showSubscribersModalForGroup.value = null
subscribers.value = []
}
async function loadSubscribers(groupId) {
try {
isLoadingSubscribers.value = true
const response = await $fetch(`/api/newsletter/groups/${groupId}/subscribers/list`)
subscribers.value = response.subscribers || []
} catch (error) {
console.error('Fehler beim Laden der Abonnenten:', error)
alert(error.data?.statusMessage || 'Fehler beim Laden der Abonnenten')
subscribers.value = []
} finally {
isLoadingSubscribers.value = false
}
}
async function removeSubscriber(subscriberId) {
if (!confirm('Möchten Sie diesen Abonnenten wirklich entfernen?')) {
return
}
try {
await $fetch(`/api/newsletter/groups/${showSubscribersModalForGroup.value.id}/subscribers/remove`, {
method: 'POST',
body: { subscriberId }
})
await loadSubscribers(showSubscribersModalForGroup.value.id)
} catch (error) {
console.error('Fehler beim Entfernen des Abonnenten:', error)
alert(error.data?.statusMessage || 'Fehler beim Entfernen des Abonnenten')
}
}
function closeAddSubscriberModal() {
showAddSubscriberModal.value = false
addSubscriberForm.value = {
email: '',
name: '',
customMessage: ''
}
addSubscriberError.value = ''
addSubscriberSuccess.value = ''
}
async function addSubscriber() {
if (!showSubscribersModalForGroup.value) return
isAddingSubscriber.value = true
addSubscriberError.value = ''
addSubscriberSuccess.value = ''
try {
const response = await $fetch(`/api/newsletter/groups/${showSubscribersModalForGroup.value.id}/subscribers/add`, {
method: 'POST',
body: addSubscriberForm.value
})
addSubscriberSuccess.value = response.message || 'Empfänger erfolgreich hinzugefügt'
// Nach 2 Sekunden schließen und Liste aktualisieren
setTimeout(async () => {
await loadSubscribers(showSubscribersModalForGroup.value.id)
closeAddSubscriberModal()
}, 2000)
} catch (error) {
console.error('Fehler beim Hinzufügen des Empfängers:', error)
addSubscriberError.value = error.data?.statusMessage || error.message || 'Fehler beim Hinzufügen des Empfängers'
} finally {
isAddingSubscriber.value = false
}
}
async function savePost() {
if (!showPostModalForGroup.value) return
if (!postFormData.value.title || !postFormData.value.content ||
!postFormData.value.content.trim() || postFormData.value.content === '<p><br></p>') {
alert('Bitte geben Sie einen Titel und Inhalt ein.')
return
}
try {
isSendingPost.value = true
const response = await $fetch(`/api/newsletter/groups/${showPostModalForGroup.value.id}/posts/create`, {
method: 'POST',
body: {
title: postFormData.value.title,
content: postFormData.value.content
}
})
postSuccessMessage.value = 'Post erfolgreich erstellt und versendet!'
postSuccessStats.value = response.stats
await loadPostsForGroup(showPostModalForGroup.value.id)
await loadGroups() // Aktualisiere Post-Count
} catch (error) {
console.error('Fehler beim Erstellen des Posts:', error)
alert(error.data?.statusMessage || 'Fehler beim Erstellen des Posts')
} finally {
isSendingPost.value = false
}
}
</script>