Files
harheimertc/components/cms/CmsMitgliedschaftsantraege.vue
Torsten Schulz (local) 905e02debf 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.
2026-02-09 09:58:46 +01:00

308 lines
13 KiB
Vue

<template>
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl sm:text-3xl font-display font-bold text-gray-900">
Mitgliedschaftsanträge
</h2>
<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>
<!-- 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>
<!-- 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">
<span
:class="[
'px-3 py-1 rounded-full text-sm font-medium',
getStatusClass(application.status)
]"
>
{{ getStatusText(application.status) }}
</span>
<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>
<!-- 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">
<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>
<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>
<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, computed, onMounted } from 'vue'
const applications = ref([])
const loading = ref(false)
const selectedApplication = ref(null)
const hasApplications = computed(() => applications.value.length > 0)
const isReady = computed(() => !loading.value)
defineExpose({ hasApplications, isReady })
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`
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() })
</script>