1050 lines
36 KiB
Vue
1050 lines
36 KiB
Vue
<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"
|
||
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"
|
||
@click="showCreateGroupModal = true"
|
||
>
|
||
<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'"
|
||
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm"
|
||
@click="showSubscribersModal(group)"
|
||
>
|
||
<Users
|
||
:size="16"
|
||
class="inline mr-1"
|
||
/>
|
||
Abonnenten
|
||
</button>
|
||
<button
|
||
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm"
|
||
@click="showPostModal(group)"
|
||
>
|
||
<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
|
||
class="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition-colors"
|
||
@click="toggleGroupPosts(group.id)"
|
||
>
|
||
<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
|
||
class="text-sm text-gray-600 prose prose-sm max-w-none mb-3"
|
||
v-html="post.content.substring(0, 200) + (post.content.length > 200 ? '...' : '')"
|
||
/>
|
||
|
||
<!-- 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
|
||
class="w-full flex items-center justify-between text-sm text-gray-600 hover:text-gray-900 transition-colors"
|
||
@click="togglePostRecipients(post.id)"
|
||
>
|
||
<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"
|
||
class="space-y-6"
|
||
@submit.prevent="saveGroup"
|
||
>
|
||
<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"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||
Typ *
|
||
</label>
|
||
<select
|
||
v-model="groupFormData.type"
|
||
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"
|
||
@change="onGroupTypeChange"
|
||
>
|
||
<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"
|
||
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||
@click="closeGroupModal"
|
||
>
|
||
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"
|
||
class="space-y-6"
|
||
@submit.prevent="savePost"
|
||
>
|
||
<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
|
||
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||
@click="closePostModal"
|
||
>
|
||
Schließen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Formular-Buttons -->
|
||
<div
|
||
v-else
|
||
class="flex justify-end space-x-3"
|
||
>
|
||
<button
|
||
type="button"
|
||
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||
@click="closePostModal"
|
||
>
|
||
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
|
||
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm flex items-center"
|
||
@click="showAddSubscriberModal = true"
|
||
>
|
||
<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
|
||
class="text-red-600 hover:text-red-900"
|
||
title="Abonnent entfernen"
|
||
@click="removeSubscriber(subscriber.id)"
|
||
>
|
||
<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"
|
||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||
@click="closeSubscribersModal"
|
||
>
|
||
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"
|
||
class="space-y-6"
|
||
@submit.prevent="addSubscriber"
|
||
>
|
||
<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..."
|
||
/>
|
||
<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"
|
||
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||
:disabled="isAddingSubscriber"
|
||
@click="closeAddSubscriberModal"
|
||
>
|
||
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, computed, onMounted } from 'vue'
|
||
import { Plus, Loader2, Users, Trash2 } from 'lucide-vue-next'
|
||
import RichTextEditor from '~/components/RichTextEditor.vue'
|
||
|
||
const authStore = useAuthStore()
|
||
|
||
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 canCreateGroup = computed(() => {
|
||
return authStore.hasAnyRole('admin', 'vorstand', 'newsletter')
|
||
})
|
||
|
||
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>
|