Update CMS navigation links and remove membership application page

This commit modifies the Navigation component and the CMS index page to replace the "Mitglieder" link with "Mitgliederverwaltung" and updates the corresponding route. Additionally, it removes the outdated "mitgliedschaftsantraege" page, streamlining the CMS structure and improving user navigation.
This commit is contained in:
Torsten Schulz (local)
2026-02-09 09:58:46 +01:00
parent 80c2b0bfeb
commit 905e02debf
7 changed files with 1291 additions and 449 deletions

View File

@@ -69,9 +69,9 @@
</p>
</NuxtLink>
<!-- Mitglieder -->
<!-- Mitgliederverwaltung (gruppiert) -->
<NuxtLink
to="/mitgliederbereich/mitglieder"
to="/cms/mitgliederverwaltung"
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">
@@ -82,11 +82,11 @@
/>
</div>
<h2 class="ml-4 text-xl font-semibold text-gray-900">
Mitglieder
Mitgliederverwaltung
</h2>
</div>
<p class="text-gray-600">
Mitgliederliste bearbeiten
Mitgliederliste &amp; Mitgliedschaftsanträge
</p>
</NuxtLink>

View File

@@ -0,0 +1,36 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-6">
<h1 class="text-3xl font-display font-bold text-gray-900">Mitgliederverwaltung</h1>
<p class="mt-1 text-sm text-gray-500">Anträge und Mitgliederliste verwalten</p>
</div>
<!-- Mitgliedschaftsanträge oben (nur sichtbar wenn Anträge vorhanden) -->
<div v-show="antraegeRef?.hasApplications" class="mb-10">
<CmsMitgliedschaftsantraege ref="antraegeRef" />
</div>
<div v-if="antraegeRef?.hasApplications" class="border-t border-gray-300 mb-10" />
<!-- Mitgliederliste darunter -->
<CmsMitglieder />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import CmsMitglieder from '~/components/cms/CmsMitglieder.vue'
import CmsMitgliedschaftsantraege from '~/components/cms/CmsMitgliedschaftsantraege.vue'
const antraegeRef = ref(null)
definePageMeta({
middleware: 'auth',
layout: 'default'
})
useHead({
title: 'Mitgliederverwaltung CMS'
})
</script>

View File

@@ -1,420 +0,0 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- Fixed Header -->
<div class="fixed top-16 left-0 right-0 bg-white shadow-sm border-b border-gray-200 z-40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between py-3 sm:py-4">
<h1 class="text-xl sm:text-3xl font-bold text-gray-900">
Mitgliedschaftsanträge
</h1>
<button
:disabled="loading"
class="px-3 py-1.5 sm:px-4 sm:py-2 bg-primary-600 hover:bg-primary-700 disabled:bg-gray-400 text-white text-sm sm:text-base font-medium rounded-lg transition-colors"
@click="refreshApplications"
>
{{ loading ? 'Lädt...' : 'Aktualisieren' }}
</button>
</div>
</div>
</div>
<!-- Content -->
<div class="pt-20 sm:pt-24">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- Loading State -->
<div
v-if="loading"
class="text-center py-12"
>
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto" />
<p class="mt-4 text-gray-600">
Lade Anträge...
</p>
</div>
<!-- Empty State -->
<div
v-else-if="applications.length === 0"
class="text-center py-12"
>
<div class="text-gray-400 text-6xl mb-4">
📋
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">
Keine Anträge vorhanden
</h3>
<p class="text-gray-600">
Es wurden noch keine Mitgliedschaftsanträge eingereicht.
</p>
</div>
<!-- Applications List -->
<div
v-else
class="space-y-6"
>
<div
v-for="application in applications"
:key="application.id"
class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden"
>
<!-- Application Header -->
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-900">
{{ application.personalData.vorname }} {{ application.personalData.nachname }}
</h3>
<p class="text-sm text-gray-600">
Eingereicht: {{ formatDate(application.timestamp) }}
</p>
</div>
<div class="flex items-center space-x-3">
<!-- Status Badge -->
<span
:class="[
'px-3 py-1 rounded-full text-sm font-medium',
getStatusClass(application.status)
]"
>
{{ getStatusText(application.status) }}
</span>
<!-- Actions -->
<div class="flex space-x-2">
<button
class="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors"
@click="viewApplication(application)"
>
Anzeigen
</button>
<button
v-if="application.metadata.pdfGenerated"
class="px-3 py-1 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors flex items-center"
@click="downloadPDF(application.id)"
>
<svg
class="w-4 h-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
PDF
</button>
<button
v-if="application.status === 'pending'"
class="px-3 py-1 text-sm bg-green-100 hover:bg-green-200 text-green-700 rounded-lg transition-colors"
@click="approveApplication(application.id)"
>
Genehmigen
</button>
<button
v-if="application.status === 'pending'"
class="px-3 py-1 text-sm bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors"
@click="rejectApplication(application.id)"
>
Ablehnen
</button>
</div>
</div>
</div>
</div>
<!-- Application Details -->
<div class="px-6 py-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 class="text-sm font-medium text-gray-900 mb-2">
Kontaktdaten
</h4>
<div class="space-y-1 text-sm text-gray-600">
<p><strong>E-Mail:</strong> {{ application.personalData.email }}</p>
<p v-if="application.personalData.telefon_privat">
<strong>Telefon:</strong> {{ application.personalData.telefon_privat }}
</p>
<p v-if="application.personalData.telefon_mobil">
<strong>Mobil:</strong> {{ application.personalData.telefon_mobil }}
</p>
</div>
</div>
<div>
<h4 class="text-sm font-medium text-gray-900 mb-2">
Antragsdetails
</h4>
<div class="space-y-1 text-sm text-gray-600">
<p><strong>Art:</strong> {{ application.metadata.mitgliedschaftsart === 'aktiv' ? 'Aktives Mitglied' : 'Passives Mitglied' }}</p>
<p><strong>Volljährig:</strong> {{ application.metadata.isVolljaehrig ? 'Ja' : 'Nein' }}</p>
<p><strong>PDF:</strong> {{ application.metadata.pdfGenerated ? 'Generiert' : 'Nicht verfügbar' }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Application Detail Modal -->
<div
v-if="selectedApplication"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click.self="closeModal"
>
<div class="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-900">
Antrag: {{ selectedApplication.personalData.vorname }} {{ selectedApplication.personalData.nachname }}
</h2>
<button
class="text-gray-400 hover:text-gray-600"
@click="closeModal"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
<div class="px-6 py-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Personal Data -->
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">
Persönliche Daten
</h3>
<div class="space-y-2 text-sm">
<p><strong>Name:</strong> {{ selectedApplication.personalData.vorname }} {{ selectedApplication.personalData.nachname }}</p>
<p><strong>E-Mail:</strong> {{ selectedApplication.personalData.email }}</p>
<p v-if="selectedApplication.personalData.telefon_privat">
<strong>Telefon:</strong> {{ selectedApplication.personalData.telefon_privat }}
</p>
<p v-if="selectedApplication.personalData.telefon_mobil">
<strong>Mobil:</strong> {{ selectedApplication.personalData.telefon_mobil }}
</p>
</div>
</div>
<!-- Application Details -->
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">
Antragsdetails
</h3>
<div class="space-y-2 text-sm">
<p><strong>Status:</strong> {{ getStatusText(selectedApplication.status) }}</p>
<p><strong>Art:</strong> {{ selectedApplication.metadata.mitgliedschaftsart === 'aktiv' ? 'Aktives Mitglied' : 'Passives Mitglied' }}</p>
<p><strong>Volljährig:</strong> {{ selectedApplication.metadata.isVolljaehrig ? 'Ja' : 'Nein' }}</p>
<p><strong>Eingereicht:</strong> {{ formatDate(selectedApplication.timestamp) }}</p>
</div>
</div>
</div>
<!-- Actions -->
<div class="mt-6 pt-6 border-t border-gray-200">
<div class="flex justify-end space-x-3">
<button
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
@click="closeModal"
>
Schließen
</button>
<button
v-if="selectedApplication.metadata.pdfGenerated"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors flex items-center"
@click="downloadPDF(selectedApplication.id)"
>
<svg
class="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
PDF herunterladen
</button>
<button
v-if="selectedApplication.status === 'pending'"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
@click="approveApplication(selectedApplication.id)"
>
Genehmigen
</button>
<button
v-if="selectedApplication.status === 'pending'"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"
@click="rejectApplication(selectedApplication.id)"
>
Ablehnen
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const applications = ref([])
const loading = ref(false)
const selectedApplication = ref(null)
const loadApplications = async () => {
loading.value = true
try {
const response = await $fetch('/api/membership/applications')
applications.value = response
} catch (error) {
console.error('Fehler beim Laden der Anträge:', error)
window.showErrorModal('Fehler', 'Fehler beim Laden der Anträge')
} finally {
loading.value = false
}
}
const refreshApplications = () => {
loadApplications()
}
const viewApplication = (application) => {
selectedApplication.value = application
}
const closeModal = () => {
selectedApplication.value = null
}
const approveApplication = async (id) => {
window.showConfirmModal('Bestätigung erforderlich', 'Möchten Sie diesen Antrag wirklich genehmigen?', async () => {
try {
await $fetch('/api/membership/update-status', {
method: 'PUT',
body: { id, status: 'approved' }
})
await loadApplications()
window.showSuccessModal('Erfolg', 'Antrag wurde erfolgreich genehmigt')
} catch (error) {
console.error('Fehler beim Genehmigen:', error)
window.showErrorModal('Fehler', 'Fehler beim Genehmigen des Antrags')
}
})
}
const rejectApplication = async (id) => {
window.showConfirmModal('Bestätigung erforderlich', 'Möchten Sie diesen Antrag wirklich ablehnen?', async () => {
try {
await $fetch('/api/membership/update-status', {
method: 'PUT',
body: { id, status: 'rejected' }
})
await loadApplications()
window.showSuccessModal('Erfolg', 'Antrag wurde erfolgreich abgelehnt')
} catch (error) {
console.error('Fehler beim Ablehnen:', error)
window.showErrorModal('Fehler', 'Fehler beim Ablehnen des Antrags')
}
})
}
const downloadPDF = async (id) => {
try {
const filename = `beitrittserklärung_${id}.pdf`
// Direkter Download über die öffentliche Uploads-Route
const response = await fetch(`/uploads/${filename}`)
if (!response.ok) {
throw new Error('PDF nicht gefunden')
}
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
window.showSuccessModal('Erfolg', 'PDF wurde erfolgreich heruntergeladen')
} catch (error) {
console.error('Fehler beim Herunterladen:', error)
window.showErrorModal('Fehler', 'Fehler beim Herunterladen des PDFs')
}
}
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const getStatusClass = (status) => {
switch (status) {
case 'pending':
return 'bg-yellow-100 text-yellow-800'
case 'approved':
return 'bg-green-100 text-green-800'
case 'rejected':
return 'bg-red-100 text-red-800'
default:
return 'bg-gray-100 text-gray-800'
}
}
const getStatusText = (status) => {
switch (status) {
case 'pending':
return 'Ausstehend'
case 'approved':
return 'Genehmigt'
case 'rejected':
return 'Abgelehnt'
default:
return 'Unbekannt'
}
}
onMounted(() => {
loadApplications()
})
useHead({
title: 'Mitgliedschaftsanträge - CMS - Harheimer TC',
})
</script>