Files
harheimertc/pages/cms/mitgliedschaftsantraege.vue

364 lines
14 KiB
Vue

<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
@click="refreshApplications"
: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"
>
{{ 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"></div>
<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
@click="viewApplication(application)"
class="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors"
>
Anzeigen
</button>
<button
v-if="application.metadata.pdfGenerated"
@click="downloadPDF(application.id)"
class="px-3 py-1 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors flex items-center"
>
<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"></path>
</svg>
PDF
</button>
<button
v-if="application.status === 'pending'"
@click="approveApplication(application.id)"
class="px-3 py-1 text-sm bg-green-100 hover:bg-green-200 text-green-700 rounded-lg transition-colors"
>
Genehmigen
</button>
<button
v-if="application.status === 'pending'"
@click="rejectApplication(application.id)"
class="px-3 py-1 text-sm bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors"
>
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
@click="closeModal"
class="text-gray-400 hover:text-gray-600"
>
<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"></path>
</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
@click="closeModal"
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
Schließen
</button>
<button
v-if="selectedApplication.metadata.pdfGenerated"
@click="downloadPDF(selectedApplication.id)"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors flex items-center"
>
<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"></path>
</svg>
PDF herunterladen
</button>
<button
v-if="selectedApplication.status === 'pending'"
@click="approveApplication(selectedApplication.id)"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
>
Genehmigen
</button>
<button
v-if="selectedApplication.status === 'pending'"
@click="rejectApplication(selectedApplication.id)"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"
>
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)
alert('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) => {
if (confirm('Antrag genehmigen?')) {
try {
await $fetch('/api/membership/update-status', {
method: 'PUT',
body: { id, status: 'approved' }
})
await loadApplications()
alert('Antrag wurde genehmigt')
} catch (error) {
console.error('Fehler beim Genehmigen:', error)
alert('Fehler beim Genehmigen des Antrags')
}
}
}
const rejectApplication = async (id) => {
if (confirm('Antrag ablehnen?')) {
try {
await $fetch('/api/membership/update-status', {
method: 'PUT',
body: { id, status: 'rejected' }
})
await loadApplications()
alert('Antrag wurde abgelehnt')
} catch (error) {
console.error('Fehler beim Ablehnen:', error)
alert('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)
} catch (error) {
console.error('Fehler beim Herunterladen:', error)
alert('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>