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:
@@ -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 & Mitgliedschaftsanträge
|
||||
</p>
|
||||
</NuxtLink>
|
||||
|
||||
|
||||
36
pages/cms/mitgliederverwaltung.vue
Normal file
36
pages/cms/mitgliederverwaltung.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user