Refactor environment configuration for local development; update SMTP settings and add JWT secret, encryption key, and debug options. Enhance Nuxt configuration for development server and runtime settings. Introduce new membership application form with validation and PDF generation functionality. Update footer and navigation components to include new membership links. Revise user and session data in JSON files.

This commit is contained in:
Torsten Schulz (local)
2025-10-23 01:31:45 +02:00
parent de73ceb62f
commit 7cd39bb452
43 changed files with 3350 additions and 457 deletions

View File

@@ -0,0 +1,317 @@
<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.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.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 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>