membership: refactor form filling, add smoke tests and debug-guard fallback; fix mappings

This commit is contained in:
Torsten Schulz (local)
2025-10-23 14:21:05 +02:00
parent f14597006e
commit e029154a8c
9 changed files with 662 additions and 295 deletions

View File

@@ -4,11 +4,66 @@
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-6"> <h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-6">
Mitgliedschaft Mitgliedschaft
</h1> </h1>
<div class="w-24 h-1 bg-primary-600 mb-8" /> <div class="w-24 h-1 bg-primary-600 mb-8"></div>
<!-- Mitgliedschaftspläne (ohne "Noch Fragen" Box) --> <!-- Mitgliedschaftspläne (ohne "Noch Fragen" Box) -->
<div class="mb-12"> <div class="mb-12">
<MembershipNoQuestions /> <section id="membership" class="py-16 sm:py-20 bg-gradient-to-b from-gray-50 to-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
Mitgliedschaft
</h2>
<div class="w-24 h-1 bg-primary-600 mx-auto mb-6"></div>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
Werden Sie Teil unserer Tischtennis-Familie - Wählen Sie die passende Mitgliedschaft für sich
</p>
</div>
<div class="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
<!-- Mitgliedschaftspläne werden dynamisch geladen -->
</div>
<!-- Satzung Download -->
<div class="mt-16 bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
<div class="text-center mb-8">
<h3 class="text-3xl font-display font-bold text-gray-900 mb-4">
Vereinsatzung
</h3>
<p class="text-xl text-gray-600">
Laden Sie unsere aktuelle Vereinsatzung herunter
</p>
</div>
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
<a
href="/documents/satzung.pdf"
target="_blank"
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" size="20" class="lucide lucide-file-text-icon mr-2">
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"></path>
<path d="M14 2v4a2 2 0 0 0 2 2h4"></path>
<path d="M10 9H8"></path>
<path d="M16 13H8"></path>
<path d="M16 17H8"></path>
</svg>
Satzung herunterladen (PDF)
</a>
<span class="text-sm text-gray-500">oder</span>
<a
href="/satzung"
class="inline-flex items-center px-6 py-3 bg-gray-100 hover:bg-gray-200 text-gray-900 font-semibold rounded-lg transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" size="20" class="lucide lucide-eye-icon mr-2">
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
Online ansehen
</a>
</div>
</div>
</div>
</section>
</div> </div>
<!-- Aufnahmeantrag Formular --> <!-- Aufnahmeantrag Formular -->
@@ -17,7 +72,7 @@
Beitrittserklärung Beitrittserklärung
</h2> </h2>
<form @submit.prevent="submitForm" class="space-y-8"> <form id="membershipForm" class="space-y-8">
<!-- Persönliche Daten --> <!-- Persönliche Daten -->
<div class="space-y-6"> <div class="space-y-6">
<h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2"> <h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2">
@@ -32,13 +87,12 @@
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p> <p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
<input <input
id="nachname" id="nachname"
v-model="form.nachname" name="nachname"
type="text" type="text"
required required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/> />
</div> </div>
<div> <div>
<label for="vorname" class="block text-sm font-medium text-gray-700 mb-2"> <label for="vorname" class="block text-sm font-medium text-gray-700 mb-2">
Vorname Vorname
@@ -46,14 +100,14 @@
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p> <p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
<input <input
id="vorname" id="vorname"
v-model="form.vorname" name="vorname"
type="text" type="text"
required required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/> />
</div> </div>
</div> </div>
<div> <div>
<label for="strasse" class="block text-sm font-medium text-gray-700 mb-2"> <label for="strasse" class="block text-sm font-medium text-gray-700 mb-2">
Straße und Hausnummer Straße und Hausnummer
@@ -61,13 +115,13 @@
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p> <p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
<input <input
id="strasse" id="strasse"
v-model="form.strasse" name="strasse"
type="text" type="text"
required required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/> />
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label for="plz" class="block text-sm font-medium text-gray-700 mb-2"> <label for="plz" class="block text-sm font-medium text-gray-700 mb-2">
@@ -76,14 +130,13 @@
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p> <p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
<input <input
id="plz" id="plz"
v-model="form.plz" name="plz"
type="text" type="text"
required required
pattern="[0-9]{5}" pattern="[0-9]{5}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/> />
</div> </div>
<div> <div>
<label for="ort" class="block text-sm font-medium text-gray-700 mb-2"> <label for="ort" class="block text-sm font-medium text-gray-700 mb-2">
Wohnort Wohnort
@@ -91,14 +144,14 @@
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p> <p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
<input <input
id="ort" id="ort"
v-model="form.ort" name="ort"
type="text" type="text"
required required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/> />
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label for="geburtsdatum" class="block text-sm font-medium text-gray-700 mb-2"> <label for="geburtsdatum" class="block text-sm font-medium text-gray-700 mb-2">
@@ -107,26 +160,25 @@
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p> <p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
<input <input
id="geburtsdatum" id="geburtsdatum"
v-model="form.geburtsdatum" name="geburtsdatum"
type="date" type="date"
required required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/> />
</div> </div>
<div> <div>
<label for="telefon_privat" class="block text-sm font-medium text-gray-700 mb-2"> <label for="telefon_privat" class="block text-sm font-medium text-gray-700 mb-2">
Telefon (privat) Telefon (privat)
</label> </label>
<input <input
id="telefon_privat" id="telefon_privat"
v-model="form.telefon_privat" name="telefon_privat"
type="tel" type="tel"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/> />
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-2"> <label for="email" class="block text-sm font-medium text-gray-700 mb-2">
@@ -135,20 +187,19 @@
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p> <p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
<input <input
id="email" id="email"
v-model="form.email" name="email"
type="email" type="email"
required required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/> />
</div> </div>
<div> <div>
<label for="telefon_mobil" class="block text-sm font-medium text-gray-700 mb-2"> <label for="telefon_mobil" class="block text-sm font-medium text-gray-700 mb-2">
Telefon (Mobil) Telefon (Mobil)
</label> </label>
<input <input
id="telefon_mobil" id="telefon_mobil"
v-model="form.telefon_mobil" name="telefon_mobil"
type="tel" type="tel"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/> />
@@ -161,21 +212,20 @@
<h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2"> <h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2">
Mitgliedschaftsart Mitgliedschaftsart
</h3> </h3>
<div class="space-y-3"> <div class="space-y-3">
<label class="flex items-center"> <label class="flex items-center">
<input <input
v-model="form.mitgliedschaftsart" name="mitgliedschaftsart"
type="radio" type="radio"
value="aktiv" value="aktiv"
checked
class="mr-3 text-primary-600 focus:ring-primary-500" class="mr-3 text-primary-600 focus:ring-primary-500"
/> />
<span class="text-gray-700">Aktives Mitglied</span> <span class="text-gray-700">Aktives Mitglied</span>
</label> </label>
<label class="flex items-center"> <label class="flex items-center">
<input <input
v-model="form.mitgliedschaftsart" name="mitgliedschaftsart"
type="radio" type="radio"
value="passiv" value="passiv"
class="mr-3 text-primary-600 focus:ring-primary-500" class="mr-3 text-primary-600 focus:ring-primary-500"
@@ -190,7 +240,6 @@
<h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2"> <h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2">
Beitragszahlung Beitragszahlung
</h3> </h3>
<div class="bg-gray-50 p-4 rounded-lg"> <div class="bg-gray-50 p-4 rounded-lg">
<p class="text-gray-700 mb-4"> <p class="text-gray-700 mb-4">
Den derzeitigen jährlichen Mitgliedsbeitrag in Höhe von: Den derzeitigen jährlichen Mitgliedsbeitrag in Höhe von:
@@ -204,10 +253,9 @@
bitte ich per Lastschrift jährlich von meinem Konto einzuziehen. bitte ich per Lastschrift jährlich von meinem Konto einzuziehen.
</p> </p>
</div> </div>
<label class="flex items-start"> <label class="flex items-start">
<input <input
v-model="form.lastschrift_erlaubt" name="lastschrift_erlaubt"
type="checkbox" type="checkbox"
required required
class="mr-3 mt-1 text-primary-600 focus:ring-primary-500" class="mr-3 mt-1 text-primary-600 focus:ring-primary-500"
@@ -234,7 +282,7 @@
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p> <p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
<input <input
id="kontoinhaber" id="kontoinhaber"
v-model="form.kontoinhaber" name="kontoinhaber"
type="text" type="text"
required required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
@@ -248,7 +296,7 @@
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p> <p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
<input <input
id="iban" id="iban"
v-model="form.iban" name="iban"
type="text" type="text"
required required
placeholder="DE89 3704 0044 0532 0130 00" placeholder="DE89 3704 0044 0532 0130 00"
@@ -262,7 +310,7 @@
</label> </label>
<input <input
id="bic" id="bic"
v-model="form.bic" name="bic"
type="text" type="text"
placeholder="COBADEFFXXX" placeholder="COBADEFFXXX"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
@@ -275,7 +323,7 @@
</label> </label>
<input <input
id="bank" id="bank"
v-model="form.bank" name="bank"
type="text" type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/> />
@@ -287,7 +335,6 @@
<h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2"> <h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2">
Datenschutz und Einverständniserklärung Datenschutz und Einverständniserklärung
</h3> </h3>
<div class="bg-blue-50 p-4 rounded-lg"> <div class="bg-blue-50 p-4 rounded-lg">
<p class="text-sm text-gray-700 mb-4"> <p class="text-sm text-gray-700 mb-4">
Der Vereinsvorstand weist darauf hin, dass ausreichende technische Maßnahmen zur Gewährleistung des Datenschutzes getroffen wurden. Dennoch kann bei einer Veröffentlichung von personenbezogenen Mitgliederdaten im Internet ein umfassender Datenschutz nicht garantiert werden. Der Vereinsvorstand weist darauf hin, dass ausreichende technische Maßnahmen zur Gewährleistung des Datenschutzes getroffen wurden. Dennoch kann bei einer Veröffentlichung von personenbezogenen Mitgliederdaten im Internet ein umfassender Datenschutz nicht garantiert werden.
@@ -295,7 +342,7 @@
<label class="flex items-start"> <label class="flex items-start">
<input <input
v-model="form.datenschutz_einverstanden" name="datenschutz_einverstanden"
type="checkbox" type="checkbox"
required required
class="mr-3 mt-1 text-primary-600 focus:ring-primary-500" class="mr-3 mt-1 text-primary-600 focus:ring-primary-500"
@@ -315,10 +362,9 @@
<h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2"> <h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2">
Vereinssatzung Vereinssatzung
</h3> </h3>
<label class="flex items-start"> <label class="flex items-start">
<input <input
v-model="form.satzung_anerkannt" name="satzung_anerkannt"
type="checkbox" type="checkbox"
required required
class="mr-3 mt-1 text-primary-600 focus:ring-primary-500" class="mr-3 mt-1 text-primary-600 focus:ring-primary-500"
@@ -346,9 +392,8 @@
<div class="flex flex-col sm:flex-row gap-4 justify-center pt-6"> <div class="flex flex-col sm:flex-row gap-4 justify-center pt-6">
<button <button
type="button" type="button"
@click="fillWithDummyData" id="fillDummyData"
:disabled="isGenerating" class="px-6 py-3 bg-gray-600 hover:bg-gray-700 text-white font-semibold rounded-lg transition-colors flex items-center justify-center"
class="px-6 py-3 bg-gray-600 hover:bg-gray-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors flex items-center justify-center"
> >
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
@@ -358,14 +403,14 @@
<button <button
type="submit" type="submit"
:disabled="!isFormValid || isGenerating" id="submitBtn"
class="px-8 py-3 bg-primary-600 hover:bg-primary-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors flex items-center justify-center" class="px-8 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors flex items-center justify-center"
> >
<svg v-if="isGenerating" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg v-if="isGenerating" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> </svg>
{{ isGenerating ? 'Formular wird erstellt...' : 'Beitrittsformular erstellen' }} Beitrittsformular erstellen
</button> </button>
</div> </div>
</form> </form>
@@ -379,184 +424,97 @@
<p class="text-xl text-primary-100 mb-6"> <p class="text-xl text-primary-100 mb-6">
Kontaktieren Sie uns - wir beraten Sie gerne persönlich Kontaktieren Sie uns - wir beraten Sie gerne persönlich
</p> </p>
<NuxtLink <a
to="/kontakt" href="/kontakt"
class="inline-flex items-center px-8 py-4 bg-white text-primary-600 font-semibold rounded-lg hover:bg-gray-100 transition-colors" class="inline-flex items-center px-8 py-4 bg-white text-primary-600 font-semibold rounded-lg hover:bg-gray-100 transition-colors"
> >
Jetzt Kontakt aufnehmen Jetzt Kontakt aufnehmen
</NuxtLink> </a>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' // Einfaches JavaScript ohne Vue.js-Reaktivität
onMounted(() => {
const form = document.getElementById('membershipForm')
const fillDummyBtn = document.getElementById('fillDummyData')
const submitBtn = document.getElementById('submitBtn')
const form = ref({ // Testdaten füllen
// Persönliche Daten fillDummyBtn.addEventListener('click', () => {
nachname: '', document.getElementById('nachname').value = 'Mustermann'
vorname: '', document.getElementById('vorname').value = 'Max'
strasse: '', document.getElementById('strasse').value = 'Musterstraße 123'
plz: '', document.getElementById('plz').value = '60437'
ort: '', document.getElementById('ort').value = 'Frankfurt am Main'
geburtsdatum: '', document.getElementById('geburtsdatum').value = '1990-05-15'
telefon_privat: '', document.getElementById('telefon_privat').value = '069 12345678'
email: '', document.getElementById('email').value = 'max.mustermann@example.com'
telefon_mobil: '', document.getElementById('telefon_mobil').value = '0171 1234567'
document.getElementById('kontoinhaber').value = 'Max Mustermann'
// Mitgliedschaftsart document.getElementById('iban').value = 'DE89 3704 0044 0532 0130 00'
mitgliedschaftsart: 'aktiv', document.getElementById('bic').value = 'COBADEFFXXX'
document.getElementById('bank').value = 'Commerzbank AG'
// Bankdaten document.querySelector('input[name="lastschrift_erlaubt"]').checked = true
kontoinhaber: '', document.querySelector('input[name="datenschutz_einverstanden"]').checked = true
iban: '', document.querySelector('input[name="satzung_anerkannt"]').checked = true
bic: '', })
bank: '',
// Einverständnisse
lastschrift_erlaubt: false,
datenschutz_einverstanden: false,
satzung_anerkannt: false
})
const isGenerating = ref(false) // Formular absenden
form.addEventListener('submit', async (e) => {
const isFormValid = computed(() => { e.preventDefault()
return form.value.nachname &&
form.value.vorname &&
form.value.strasse &&
form.value.plz &&
form.value.ort &&
form.value.geburtsdatum &&
form.value.email &&
form.value.mitgliedschaftsart &&
form.value.kontoinhaber &&
form.value.iban &&
form.value.lastschrift_erlaubt &&
form.value.datenschutz_einverstanden &&
form.value.satzung_anerkannt
})
const isVolljaehrig = computed(() => {
if (!form.value.geburtsdatum) return true // Default zu volljährig
const heute = new Date()
const geburtsdatum = new Date(form.value.geburtsdatum)
const alter = heute.getFullYear() - geburtsdatum.getFullYear()
const monatDiff = heute.getMonth() - geburtsdatum.getMonth()
if (monatDiff < 0 || (monatDiff === 0 && heute.getDate() < geburtsdatum.getDate())) {
return alter - 1 >= 18
}
return alter >= 18
})
const fillWithDummyData = () => {
// Dummy-Daten für Testzwecke
form.value = {
// Persönliche Daten
nachname: 'Mustermann',
vorname: 'Max',
strasse: 'Musterstraße 123',
plz: '60437',
ort: 'Frankfurt am Main',
geburtsdatum: '1990-05-15', // Volljährig
telefon_privat: '069 12345678',
email: 'max.mustermann@example.com',
telefon_mobil: '0171 1234567',
// Mitgliedschaftsart submitBtn.disabled = true
mitgliedschaftsart: 'aktiv', submitBtn.textContent = 'Formular wird erstellt...'
// Bankdaten try {
kontoinhaber: 'Max Mustermann', const formData = new FormData(form)
iban: 'DE89 3704 0044 0532 0130 00', const data = Object.fromEntries(formData.entries())
bic: 'COBADEFFXXX',
bank: 'Commerzbank AG',
// Einverständnisse
lastschrift_erlaubt: true,
datenschutz_einverstanden: true,
satzung_anerkannt: true
}
}
const submitForm = async () => {
if (!isFormValid.value) return
isGenerating.value = true
try {
const response = await $fetch('/api/membership/generate-pdf', {
method: 'POST',
body: {
...form.value,
isVolljaehrig: isVolljaehrig.value
}
})
if (response.success) {
// PDF herunterladen über geschützten Endpoint
try {
const downloadResponse = await $fetch(response.downloadUrl, {
method: 'GET',
responseType: 'blob'
})
// Blob zu Download-Link konvertieren
const blob = new Blob([downloadResponse], {
type: 'application/pdf'
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `Beitrittserklärung_${form.value.nachname}_${form.value.vorname}.pdf`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
// Erfolgsmeldung
alert('Beitrittsformular wurde erfolgreich erstellt und per E-Mail an den Vorstand/Trainer gesendet!')
} catch (downloadError) {
console.error('Download-Fehler:', downloadError)
alert('Formular wurde erfolgreich erstellt und per E-Mail an den Vorstand/Trainer gesendet!\n\nFalls der Download fehlschlägt, können Sie das Formular auch später über den Link in der E-Mail herunterladen.')
}
// Formular zurücksetzen // Volljährigkeit prüfen
form.value = { const birthDate = new Date(data.geburtsdatum)
nachname: '', const today = new Date()
vorname: '', const age = today.getFullYear() - birthDate.getFullYear()
strasse: '', const monthDiff = today.getMonth() - birthDate.getMonth()
plz: '', data.isVolljaehrig = age > 18 || (age === 18 && monthDiff >= 0)
ort: '',
geburtsdatum: '', const response = await fetch('/api/membership/generate-pdf', {
telefon_privat: '', method: 'POST',
email: '', headers: {
telefon_mobil: '', 'Content-Type': 'application/json'
mitgliedschaftsart: 'aktiv', },
kontoinhaber: '', body: JSON.stringify(data)
iban: '', })
bic: '',
bank: '', const result = await response.json()
lastschrift_erlaubt: false,
datenschutz_einverstanden: false, if (result.success) {
satzung_anerkannt: false // PDF herunterladen
const downloadResponse = await fetch(result.downloadUrl)
const blob = await downloadResponse.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `beitrittserklärung_${Date.now()}.pdf`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
alert('Beitrittsformular erfolgreich erstellt und heruntergeladen!')
} else {
alert('Fehler beim Erstellen des Formulars: ' + (result.error || 'Unbekannter Fehler'))
} }
} else { } catch (error) {
alert('Fehler beim Erstellen des Formulars: ' + (response.error || 'Unbekannter Fehler')) console.error('Fehler:', error)
alert('Fehler beim Senden des Formulars: ' + error.message)
} finally {
submitBtn.disabled = false
submitBtn.textContent = 'Beitrittsformular erstellen'
} }
} catch (error) { })
console.error('Fehler:', error)
alert('Fehler beim Erstellen des Formulars. Bitte versuchen Sie es erneut.')
} finally {
isGenerating.value = false
}
}
useHead({
title: 'Mitgliedschaft - Harheimer TC',
}) })
</script> </script>

View File

@@ -507,11 +507,19 @@ Das Vereinsmitglied trifft die Entscheidung zur Veröffentlichung seiner Daten i
const sigLabelSize = 10 const sigLabelSize = 10
page3.drawText(sigLabel, { x: dateX, y: dateFieldY - 18, size: sigLabelSize, font: helv }) page3.drawText(sigLabel, { x: dateX, y: dateFieldY - 18, size: sigLabelSize, font: helv })
// Ensure appearance streams are generated for all form fields using the embedded font
try {
form.updateFieldAppearances(helv)
} catch (e) {
console.warn('Warning: updateFieldAppearances failed while generating template:', e)
}
const pdfBytes = await pdfDoc.save() const pdfBytes = await pdfDoc.save()
fs.writeFileSync('server/templates/mitgliedschaft-fillable.pdf', pdfBytes) fs.writeFileSync('server/templates/mitgliedschaft-fillable.pdf', pdfBytes)
console.log('Wrote server/templates/mitgliedschaft-fillable.pdf') console.log('Wrote server/templates/mitgliedschaft-fillable.pdf')
} }
create().catch(e => { create().catch(e => {
console.error(e) console.error(e)
process.exit(1) process.exit(1)

View File

@@ -12,6 +12,9 @@ async function fill() {
const pdfDoc = await PDFDocument.load(existingPdfBytes) const pdfDoc = await PDFDocument.load(existingPdfBytes)
const form = pdfDoc.getForm() const form = pdfDoc.getForm()
// Ensure a readable font is embedded and used for field appearances
const helv = await pdfDoc.embedFont(StandardFonts.Helvetica)
// Simple sample data // Simple sample data
const sample = { const sample = {
nachname: 'Müller', nachname: 'Müller',
@@ -51,45 +54,200 @@ async function fill() {
} }
} }
safeSetText('nachname', sample.nachname) // Robust setter: find a field by name (case-insensitive) and set text/checkbox/select accordingly
safeSetText('vorname', sample.vorname) function setFieldByName(name, value) {
safeSetText('strasse', sample.strasse) try {
safeSetText('plz_ort', sample.plz_ort) const lower = name.toLowerCase()
safeSetText('geburtsdatum', sample.geburtsdatum) const field = form.getFields().find(f => f.getName() && f.getName().toLowerCase() === lower)
safeSetText('telefon', sample.telefon) if (!field) {
safeSetText('email', sample.email) console.log(`DEBUG: Field not found for '${name}'`)
safeSetText('telefon_mobil', sample.telefon_mobil) return false
}
// Text field
if (typeof field.setText === 'function') {
field.setText(value == null ? '' : String(value))
return true
}
// Check box
if (typeof field.check === 'function') {
if (value === true || String(value).toLowerCase() === 'true') field.check()
else if (typeof field.uncheck === 'function') field.uncheck()
return true
}
// Radio/select (pdf-lib uses select for dropdowns)
if (typeof field.select === 'function') {
try { field.select(String(value)) } catch (e) { /* ignore */ }
return true
}
console.log(`DEBUG: Unsupported field type for '${name}'`)
return false
} catch (e) {
console.log(`DEBUG: Error setting field '${name}':`, e.message)
return false
}
}
// Debug: list all form fields found in the template
try { try {
const cbA = form.getCheckBox('mitglied_aktiv') const allFields = form.getFields().map(f => f.getName())
if (sample.mitglied_aktiv) cbA.check(); else cbA.uncheck() console.log('DEBUG: Template form fields:', allFields.join(', '))
} catch(e) {} } catch (e) {
try { console.log('DEBUG: Could not list form fields:', e.message)
const cbP = form.getCheckBox('mitglied_passiv') }
if (sample.mitglied_passiv) cbP.check(); else cbP.uncheck()
} catch(e) {}
safeSetText('sepa_mitglied', sample.sepa_mitglied) setFieldByName('nachname', sample.nachname)
safeSetText('sepa_kontoinhaber', sample.sepa_kontoinhaber) setFieldByName('vorname', sample.vorname)
safeSetText('sepa_strasse', sample.sepa_strasse) setFieldByName('strasse', sample.strasse)
safeSetText('sepa_plz_ort', sample.sepa_plz_ort) setFieldByName('plz_ort', sample.plz_ort)
safeSetText('sepa_bank', sample.sepa_bank) setFieldByName('geburtsdatum', sample.geburtsdatum)
safeSetText('sepa_iban', sample.sepa_iban) setFieldByName('telefon', sample.telefon)
safeSetText('sepa_bic', sample.sepa_bic) setFieldByName('email', sample.email)
safeSetText('sepa_datum', sample.sepa_datum) setFieldByName('telefon_mobil', sample.telefon_mobil)
safeSetText('sign_datum', sample.sign_datum)
// Checkboxes via robust setter
setFieldByName('mitglied_aktiv', sample.mitglied_aktiv)
setFieldByName('mitglied_passiv', sample.mitglied_passiv)
setFieldByName('sepa_mitglied', sample.sepa_mitglied)
setFieldByName('sepa_kontoinhaber', sample.sepa_kontoinhaber)
setFieldByName('sepa_strasse', sample.sepa_strasse)
setFieldByName('sepa_plz_ort', sample.sepa_plz_ort)
setFieldByName('sepa_bank', sample.sepa_bank)
setFieldByName('sepa_iban', sample.sepa_iban)
setFieldByName('sepa_bic', sample.sepa_bic)
setFieldByName('sepa_datum', sample.sepa_datum)
setFieldByName('sign_datum', sample.sign_datum)
// page3 fields // page3 fields
safeSetText('page3_name', sample.page3_name) setFieldByName('page3_name', sample.page3_name)
safeSetText('page3_vorname', sample.page3_vorname) setFieldByName('page3_vorname', sample.page3_vorname)
safeSetText('page3_anschrift', sample.page3_anschrift) setFieldByName('page3_anschrift', sample.page3_anschrift)
safeSetText('page3_telefon', sample.page3_telefon) setFieldByName('page3_telefon', sample.page3_telefon)
safeSetText('page3_fax', sample.page3_fax) setFieldByName('page3_fax', sample.page3_fax)
safeSetText('page3_email', sample.page3_email) setFieldByName('page3_email', sample.page3_email)
safeSetText('page3_datum', sample.page3_datum) setFieldByName('page3_datum', sample.page3_datum)
// flatten all fields // Debug: check which sample keys correspond to actual fields
try { form.flatten() } catch (e) {} try {
const names = form.getFields().map(f => f.getName().toLowerCase())
for (const key of Object.keys(sample)) {
const found = names.includes(key.toLowerCase())
console.log(`DEBUG: sample key='${key}' -> field present=${found}`)
}
} catch (e) {
console.log('DEBUG: field presence check failed:', e.message)
}
// Debug: read back all field values after setting (before flattening)
try {
console.log('DEBUG: Field values after setting:')
for (const f of form.getFields()) {
const name = f.getName()
let val = null
try {
if (typeof f.getText === 'function') val = f.getText()
else if (typeof f.isChecked === 'function') val = f.isChecked()
else val = '(no getter)'
} catch (e) {
val = `(error reading: ${e.message})`
}
console.log(` ${name}: ${val}`)
}
} catch (e) {
console.log('DEBUG: Could not read back field values:', e.message)
}
// Debug: print widget rectangles for relevant fields (SEPA and page3)
try {
const interesting = ['sepa_mitglied','sepa_kontoinhaber','sepa_strasse','sepa_plz_ort','sepa_bank','sepa_iban','sepa_bic','page3_name','page3_vorname','page3_anschrift','page3_telefon','page3_email']
for (const fname of interesting) {
const f = form.getFields().find(x => x.getName && x.getName().toLowerCase() === fname)
if (!f) { console.log(`DEBUG: no field object for ${fname}`); continue }
try {
// attempt to access widget rectangle via low-level acroField
const acro = f.acroField
const widgets = acro.getWidgets()
if (!widgets || widgets.length === 0) { console.log(`DEBUG: no widgets for ${fname}`); continue }
const rect = widgets[0].getRectangle()
console.log(`DEBUG: widget rect for ${fname}: ${JSON.stringify(rect)}`)
} catch (e) {
console.log(`DEBUG: cannot read widget rect for ${fname}: ${e.message}`)
}
}
} catch (e) {
console.log('DEBUG: widget rect inspection failed:', e.message)
}
// Define fallback drawing: draw visible text directly onto pages at widget rect positions
async function fallbackDraw() {
try {
const pages = pdfDoc.getPages()
// draw SEPA fields on page 2 (index 1)
const p2 = pages[1]
const sepaFields = ['sepa_mitglied','sepa_kontoinhaber','sepa_strasse','sepa_plz_ort','sepa_bank','sepa_iban','sepa_bic']
for (const fname of sepaFields) {
const f = form.getFields().find(x => x.getName && x.getName().toLowerCase() === fname)
if (!f) continue
try {
const widgets = f.acroField.getWidgets()
if (!widgets || widgets.length === 0) continue
const rect = widgets[0].getRectangle()
const text = (typeof f.getText === 'function') ? f.getText() : ''
if (text) {
p2.drawText(String(text), { x: rect.x + 2, y: rect.y + rect.height - 12, size: 11, font: helv })
console.log(`FALLBACK: drew ${fname} on page2 at ${rect.x},${rect.y}`)
}
} catch (e) {
console.log(`FALLBACK: could not draw ${fname}: ${e.message}`)
}
}
// draw page3 fields on page 3 (index 2)
const p3 = pages[2]
const p3Fields = ['page3_name','page3_vorname','page3_anschrift','page3_telefon','page3_email']
for (const fname of p3Fields) {
const f = form.getFields().find(x => x.getName && x.getName().toLowerCase() === fname)
if (!f) continue
try {
const widgets = f.acroField.getWidgets()
if (!widgets || widgets.length === 0) continue
const rect = widgets[0].getRectangle()
const text = (typeof f.getText === 'function') ? f.getText() : ''
if (text) {
p3.drawText(String(text), { x: rect.x + 2, y: rect.y + rect.height - 12, size: 11, font: helv })
console.log(`FALLBACK: drew ${fname} on page3 at ${rect.x},${rect.y}`)
}
} catch (e) {
console.log(`FALLBACK: could not draw ${fname}: ${e.message}`)
}
}
// write fallback copy
const fallbackBytes = await pdfDoc.save()
if (!fs.existsSync('temp')) fs.mkdirSync('temp')
fs.writeFileSync('temp/mitgliedschaft-sample-filled-fallback.pdf', fallbackBytes)
console.log('Wrote temp/mitgliedschaft-sample-filled-fallback.pdf')
} catch (e) {
console.log('FALLBACK drawing failed:', e.message)
}
}
// Update field appearances so text is visible, then flatten. Run fallback only when enabled.
try {
form.updateFieldAppearances(helv)
const outUnflattened = await pdfDoc.save()
if (!fs.existsSync('temp')) fs.mkdirSync('temp')
fs.writeFileSync('temp/mitgliedschaft-sample-filled-unflattened.pdf', outUnflattened)
form.flatten()
} catch (e) {
console.warn('Could not update field appearances:', e.message)
const enableFallback = process.env.ENABLE_FALLBACK === '1' || (typeof sample.debug !== 'undefined' && sample.debug)
if (enableFallback) {
try { await fallbackDraw() } catch (err) { console.warn('Fallback draw failed:', err.message) }
try { form.flatten() } catch (err) { /* ignore */ }
}
}
const out = await pdfDoc.save() const out = await pdfDoc.save()
if (!fs.existsSync('temp')) fs.mkdirSync('temp') if (!fs.existsSync('temp')) fs.mkdirSync('temp')

View File

@@ -0,0 +1,40 @@
import fs from 'fs'
import path from 'path'
import { PDFDocument } from 'pdf-lib'
async function main() {
const uploads = path.join(process.cwd(), 'public', 'uploads')
const files = fs.existsSync(uploads) ? fs.readdirSync(uploads).filter(f => f.toLowerCase().endsWith('.pdf')) : []
if (files.length === 0) { console.log('no pdfs'); return }
files.sort((a,b) => fs.statSync(path.join(uploads,b)).mtimeMs - fs.statSync(path.join(uploads,a)).mtimeMs)
const latest = path.join(uploads, files[0])
console.log('Inspecting', latest)
const bytes = fs.readFileSync(latest)
const pdf = await PDFDocument.load(bytes)
let form
try { form = pdf.getForm() } catch (e) { form = null }
if (!form) { console.log('no form'); return }
const fields = form.getFields()
const matches = []
for (const f of fields) {
const name = f.getName()
try {
if (typeof f.getText === 'function') {
const v = f.getText()
if (v && (String(v).toLowerCase() === 'aktiv' || String(v).toLowerCase() === 'passiv')) {
matches.push({ name, value: v })
}
} else if (typeof f.isChecked === 'function') {
const checked = f.isChecked()
if (checked) {
// value true -> possibly membership
matches.push({ name, value: 'checked' })
}
}
} catch (e) {}
}
if (matches.length === 0) console.log('no aktiv/passiv values found')
else console.log('matches:', matches)
}
main().catch(e => { console.error(e); process.exit(1) })

77
scripts/inspect-forms.js Normal file
View File

@@ -0,0 +1,77 @@
import fs from 'fs'
import path from 'path'
import { PDFDocument, StandardFonts } from 'pdf-lib'
async function inspect(pdfPath) {
console.log('\n--- Inspecting', pdfPath)
if (!fs.existsSync(pdfPath)) {
console.log('MISSING:', pdfPath)
return
}
const bytes = fs.readFileSync(pdfPath)
const pdfDoc = await PDFDocument.load(bytes)
let form = null
try { form = pdfDoc.getForm() } catch (e) { form = null }
if (!form) { console.log('No AcroForm found') ; return }
const fields = form.getFields()
console.log('Field count:', fields.length)
for (const f of fields) {
const name = f.getName()
let value = null
try {
if (typeof f.getText === 'function') value = f.getText()
else if (typeof f.isChecked === 'function') value = f.isChecked()
else value = '(no getter)'
} catch (e) {
value = `(error reading: ${e.message})`
}
// widgets
let widgetsInfo = []
try {
const acro = f.acroField
const widgets = acro.getWidgets()
for (const w of widgets) {
try {
const rect = w.getRectangle()
// try to find page index by searching pages for an annotation with same ref
let pageIndex = null
try {
const pages = pdfDoc.getPages()
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
const annots = page.node.Annots ? page.node.Annots() : null
// can't reliably map here; just record rect
}
} catch (e) {}
widgetsInfo.push({ rect })
} catch (e) {
widgetsInfo.push({ error: e.message })
}
}
} catch (e) {
widgetsInfo = [`error widgets: ${e.message}`]
}
console.log(`- ${name}: value='${value}' widgets=${widgetsInfo.length}`)
for (const wi of widgetsInfo) console.log(' ', JSON.stringify(wi))
}
}
async function main() {
const repoRoot = process.cwd()
const template = path.join(repoRoot, 'server', 'templates', 'mitgliedschaft-fillable.pdf')
// pick latest generated PDF in public/uploads that is not the sample
const uploads = path.join(repoRoot, 'public', 'uploads')
let pdfFiles = []
if (fs.existsSync(uploads)) {
pdfFiles = fs.readdirSync(uploads).filter(f => f.toLowerCase().endsWith('.pdf'))
.map(f => ({ f, mtime: fs.statSync(path.join(uploads, f)).mtimeMs }))
.sort((a,b) => b.mtime - a.mtime)
.map(x => x.f)
}
const apiPdf = pdfFiles.find(n => !n.includes('sample')) || pdfFiles[0]
await inspect(template)
if (apiPdf) await inspect(path.join(uploads, apiPdf))
else console.log('No API-generated PDF found in public/uploads')
}
main().catch(e => { console.error(e); process.exit(1) })

25
scripts/smoke-test.js Normal file
View File

@@ -0,0 +1,25 @@
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
function run(cmd) {
console.log('> ', cmd)
try { const out = execSync(cmd, { stdio: 'pipe' }).toString(); console.log(out); return out } catch (e) { console.error('ERROR:', e.message); return null }
}
async function main() {
const root = process.cwd()
run('node scripts/create-fillable-template.js')
run('node scripts/fill-sample-template.js')
const uploads = path.join(root, 'public', 'uploads')
const files = fs.existsSync(uploads) ? fs.readdirSync(uploads).filter(f => f.toLowerCase().endsWith('.pdf')) : []
console.log('Uploads PDFs:', files)
// try API if server env present
const apiUrl = process.env.MEMBERSHIP_API_URL || ''
if (apiUrl) {
run(`curl -sS -X POST "${apiUrl}" -H 'Content-Type: application/json' -d '{"nachname":"Test","vorname":"Smoke","strasse":"X","plz":"00000","ort":"Local","geburtsdatum":"1990-01-01","telefon_privat":"000","telefon_mobil":"000","email":"x@example.com","mitgliedschaftsart":"aktiv","kontoinhaber":"Smoke Test","iban":"DE00","bic":"XXXX","bank":"Local"}' -D - | sed -n '1,80p'`)
}
run('node scripts/inspect-forms.js')
}
main()

View File

@@ -45,13 +45,22 @@ export default defineEventHandler(async (event) => {
} }
} }
// Prüfen ob es sich um eine aktuelle Session handelt (innerhalb der letzten 30 Minuten) // Prüfen ob es sich um eine aktuelle Session handelt (innerhalb der letzten 24 Stunden)
const sessionKey = `download_${fileId}` const downloadToken = getCookie(event, 'download_token')
const sessionValue = getCookie(event, sessionKey)
if (sessionValue === 'authorized') { if (downloadToken) {
// Session-basierte Berechtigung für Antragsteller try {
isAuthorized = true const decoded = Buffer.from(downloadToken, 'base64').toString('utf8')
const [tokenFilename, timestamp] = decoded.split(':')
// Prüfen ob der Token für diese Datei ist und nicht älter als 24 Stunden
if (tokenFilename === fileId.replace('.pdf', '') &&
Date.now() - parseInt(timestamp) < 24 * 60 * 60 * 1000) {
isAuthorized = true
}
} catch (e) {
console.warn('Ungültiger Download-Token:', e.message)
}
} }
if (!isAuthorized) { if (!isAuthorized) {

View File

@@ -9,6 +9,81 @@ import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
const execAsync = promisify(exec) const execAsync = promisify(exec)
function mapFieldValue(data, name) {
name = name.toLowerCase()
if (name.includes('sepa_mitglied')) return `${data.vorname || ''} ${data.nachname || ''}`.trim()
if (name.includes('sepa_kontoinhaber')) return data.kontoinhaber || `${data.vorname || ''} ${data.nachname || ''}`.trim()
if (name.includes('sepa_plz_ort')) return `${data.plz || ''} ${data.ort || ''}`.trim()
if (name.includes('page3_anschrift')) return `${data.strasse || ''}, ${data.plz || ''} ${data.ort || ''}`.trim()
if (name.includes('nachname') || name.includes('zuname') || name.includes('name')) return data.nachname || ''
if (name.includes('vorname') || name.includes('given')) return data.vorname || ''
if (name.includes('str') || name.includes('straße') || name.includes('street')) return data.strasse || ''
if (name.includes('plz')) return data.plz || ''
if (name.includes('ort') || name.includes('stadt')) return data.ort || ''
if (name.includes('geb') || name.includes('geburts')) return new Date(data.geburtsdatum).toLocaleDateString('de-DE')
if (name.includes('telefon') || name.includes('tel')) return data.telefon_privat || data.telefon_mobil || ''
if (name.includes('email')) return data.email || ''
if (name.includes('datum')) return data.sign_datum || data.sepa_datum || data.page3_datum || new Date().toLocaleDateString('de-DE')
if (name.includes('kontoinhaber') || name.includes('kontoinh')) return data.kontoinhaber || ''
if (name.includes('iban')) return data.iban || ''
if (name.includes('bic')) return data.bic || ''
if (name.includes('bank') || name.includes('kreditinstitut')) return data.bank || ''
if (name.includes('mitgliedschaft') || name.includes('art')) return data.mitgliedschaftsart || ''
return ''
}
function setTextFieldIfEmpty(field, val) {
if (typeof field.setText !== 'function') return
try {
if (typeof field.getText === 'function') {
const cur = field.getText()
if (cur && String(cur).trim() !== '') return
}
} catch (e) {}
if (val != null && String(val).trim() !== '') field.setText(val)
}
function setCheckboxIfNeeded(field, name, data) {
if (!(typeof field.check === 'function' || typeof field.isChecked === 'function')) return
const lower = name.toLowerCase()
try {
if (lower.includes('aktiv') || lower.includes('passiv') || lower.includes('mitglied')) {
if (typeof field.isChecked === 'function' && field.isChecked()) return
if (data.mitgliedschaftsart && lower.includes(data.mitgliedschaftsart)) { field.check && field.check(); return }
if (lower.includes('aktiv') && data.mitgliedschaftsart === 'aktiv') field.check && field.check()
if (lower.includes('passiv') && data.mitgliedschaftsart === 'passiv') field.check && field.check()
return
}
const mapped = mapFieldValue(data, lower)
if (mapped === 'true' || mapped === 'ja' || mapped === 'checked') {
try {
if (!(typeof field.isChecked === 'function' && field.isChecked())) field.check && field.check()
} catch (e) { field.check && field.check() }
}
} catch (e) {}
}
async function fillFormFields(pdfDoc, form, data) {
const fields = form.getFields()
for (const field of fields) {
const fname = field.getName()
const lower = fname.toLowerCase()
if (typeof field.setText === 'function') {
const val = mapFieldValue(data, lower)
setTextFieldIfEmpty(field, val)
continue
}
if (typeof field.check === 'function' || typeof field.isChecked === 'function') {
setCheckboxIfNeeded(field, lower, data)
continue
}
}
try {
const helv2 = await pdfDoc.embedFont(StandardFonts.Helvetica)
form.updateFieldAppearances(helv2)
} catch (e) {}
}
function generateLaTeXContent(data) { function generateLaTeXContent(data) {
const heute = new Date().toLocaleDateString('de-DE') const heute = new Date().toLocaleDateString('de-DE')
@@ -386,11 +461,11 @@ export default defineEventHandler(async (event) => {
// Membership checkbox positions (approx.) // Membership checkbox positions (approx.)
mitglied_checkbox_aktiv: { x: leftX - 40, y: baseY - gap * 6 + yOffset }, mitglied_checkbox_aktiv: { x: leftX - 40, y: baseY - gap * 6 + yOffset },
mitglied_checkbox_passiv: { x: leftX - 40, y: baseY - gap * 7 + yOffset }, mitglied_checkbox_passiv: { x: leftX - 40, y: baseY - gap * 7 + yOffset },
// Account details on subsequent page(s) // Account details on subsequent page(s) - korrigierte Koordinaten für Seite 2
kontoinhaber: { x: leftX, y: baseY - gap * 12 + yOffset }, kontoinhaber: { x: leftX, y: baseY + yOffset },
iban: { x: leftX, y: baseY - gap * 13 + yOffset }, iban: { x: leftX, y: baseY - gap + yOffset },
bic: { x: leftX, y: baseY - gap * 14 + yOffset }, bic: { x: leftX, y: baseY - gap * 2 + yOffset },
bank: { x: leftX, y: baseY - gap * 15 + yOffset } bank: { x: leftX, y: baseY - gap * 3 + yOffset }
} }
const drawText = (page, text, x, y, size = 11) => { const drawText = (page, text, x, y, size = 11) => {
@@ -421,12 +496,13 @@ export default defineEventHandler(async (event) => {
firstPage.drawText(data.telefon_privat || data.telefon_mobil || '', { x: coords.telefon.x, y: coords.telefon.y, size: 11, font: helveticaFont }) firstPage.drawText(data.telefon_privat || data.telefon_mobil || '', { x: coords.telefon.x, y: coords.telefon.y, size: 11, font: helveticaFont })
firstPage.drawText(data.email || '', { x: coords.email.x, y: coords.email.y, size: 11, font: helveticaFont }) firstPage.drawText(data.email || '', { x: coords.email.x, y: coords.email.y, size: 11, font: helveticaFont })
firstPage.drawText(data.telefon_mobil || '', { x: coords.telefon_mobil.x, y: coords.telefon_mobil.y, size: 11, font: helveticaFont }) firstPage.drawText(data.telefon_mobil || '', { x: coords.telefon_mobil.x, y: coords.telefon_mobil.y, size: 11, font: helveticaFont })
// Kontodaten evtl. auf andere Seite: falls mehrere Seiten vorhanden, nutze last page // Bankdaten als sichtbarer Text am Ende der ersten Seite (garantiert sichtbar)
const lastPage = pages[pages.length - 1] const bottomY = 50 // Am unteren Rand der Seite
lastPage.drawText(data.kontoinhaber || '', { x: coords.kontoinhaber.x, y: coords.kontoinhaber.y, size: 11, font: helveticaFont }) firstPage.drawText(`BANKDATEN (SEITE 2):`, { x: 50, y: bottomY + 100, size: 12, font: helveticaFont, color: rgb(1, 0, 0) })
lastPage.drawText(data.iban || '', { x: coords.iban.x, y: coords.iban.y, size: 11, font: helveticaFont }) firstPage.drawText(`Kontoinhaber: ${data.kontoinhaber || ''}`, { x: 50, y: bottomY + 80, size: 11, font: helveticaFont })
lastPage.drawText(data.bic || '', { x: coords.bic.x, y: coords.bic.y, size: 11, font: helveticaFont }) firstPage.drawText(`IBAN: ${data.iban || ''}`, { x: 50, y: bottomY + 60, size: 11, font: helveticaFont })
lastPage.drawText(data.bank || '', { x: coords.bank.x, y: coords.bank.y, size: 11, font: helveticaFont }) firstPage.drawText(`BIC: ${data.bic || ''}`, { x: 50, y: bottomY + 40, size: 11, font: helveticaFont })
firstPage.drawText(`Bank: ${data.bank || ''}`, { x: 50, y: bottomY + 20, size: 11, font: helveticaFont })
// Zeichne X in die passende Mitgliedschafts-Checkbox // Zeichne X in die passende Mitgliedschafts-Checkbox
try { try {
if (data.mitgliedschaftsart === 'aktiv') { if (data.mitgliedschaftsart === 'aktiv') {
@@ -469,39 +545,16 @@ export default defineEventHandler(async (event) => {
// Wenn Formularfelder existieren: befülle sie per AcroForm // Wenn Formularfelder existieren: befülle sie per AcroForm
const fields = form.getFields() const fields = form.getFields()
if (fields && fields.length > 0) { if (fields && fields.length > 0) {
try { try { await fillFormFields(pdfDoc, form, data) } catch (e) { console.warn('AcroForm-Füllung fehlgeschlagen, fallback auf positional:', e.message) }
const byName = {}
for (const f of fields) byName[f.getName().toLowerCase()] = f
const setIf = (name, value) => {
const f = byName[name]
if (!f) return
try {
if (typeof f.setText === 'function') f.setText(String(value || ''))
else if (typeof f.check === 'function' && (value === true || String(value).toLowerCase() === 'true')) f.check()
} catch (e) {
console.warn('Fehler beim Setzen Feld', name, e.message)
}
}
setIf('nachname', data.nachname)
setIf('vorname', data.vorname)
setIf('strasse', data.strasse)
setIf('plz_ort', `${data.plz || ''} ${data.ort || ''}`.trim())
setIf('geburtsdatum', new Date(data.geburtsdatum).toLocaleDateString('de-DE'))
setIf('telefon', data.telefon_privat || data.telefon_mobil)
setIf('email', data.email)
setIf('telefon_mobil', data.telefon_mobil)
// Checkboxes
if (byName['mitglied_aktiv'] && data.mitgliedschaftsart === 'aktiv') byName['mitglied_aktiv'].check && byName['mitglied_aktiv'].check()
if (byName['mitglied_passiv'] && data.mitgliedschaftsart === 'passiv') byName['mitglied_passiv'].check && byName['mitglied_passiv'].check()
const pdfBytes = await pdfDoc.save()
return Buffer.from(pdfBytes)
} catch (e) {
console.warn('AcroForm-Füllung fehlgeschlagen, fallback auf positional:', e.message)
}
} }
const mapValue = (name) => { const mapValue = (name) => {
// einfache Heuristiken für Feldnamen // einfache Heuristiken für Feldnamen
name = name.toLowerCase() name = name.toLowerCase()
// specific overrides first
if (name.includes('sepa_mitglied')) return `${data.vorname || ''} ${data.nachname || ''}`.trim()
if (name.includes('sepa_kontoinhaber')) return data.kontoinhaber || `${data.vorname || ''} ${data.nachname || ''}`.trim()
if (name.includes('sepa_plz_ort')) return `${data.plz || ''} ${data.ort || ''}`.trim()
if (name.includes('page3_anschrift')) return `${data.strasse || ''}, ${data.plz || ''} ${data.ort || ''}`.trim()
if (name.includes('nachname') || name.includes('zuname') || name.includes('name')) return data.nachname || '' if (name.includes('nachname') || name.includes('zuname') || name.includes('name')) return data.nachname || ''
if (name.includes('vorname') || name.includes('given')) return data.vorname || '' if (name.includes('vorname') || name.includes('given')) return data.vorname || ''
if (name.includes('str') || name.includes('straße') || name.includes('street')) return data.strasse || '' if (name.includes('str') || name.includes('straße') || name.includes('street')) return data.strasse || ''
@@ -510,11 +563,15 @@ export default defineEventHandler(async (event) => {
if (name.includes('geb') || name.includes('geburts')) return new Date(data.geburtsdatum).toLocaleDateString('de-DE') if (name.includes('geb') || name.includes('geburts')) return new Date(data.geburtsdatum).toLocaleDateString('de-DE')
if (name.includes('telefon') || name.includes('tel')) return data.telefon_privat || data.telefon_mobil || '' if (name.includes('telefon') || name.includes('tel')) return data.telefon_privat || data.telefon_mobil || ''
if (name.includes('email')) return data.email || '' if (name.includes('email')) return data.email || ''
// general date fields: use provided sign/sepa/page3 date or today's date
if (name.includes('datum')) return data.sign_datum || data.sepa_datum || data.page3_datum || new Date().toLocaleDateString('de-DE')
if (name.includes('kontoinhaber') || name.includes('kontoinh')) return data.kontoinhaber || '' if (name.includes('kontoinhaber') || name.includes('kontoinh')) return data.kontoinhaber || ''
if (name.includes('iban')) return data.iban || '' if (name.includes('iban')) return data.iban || ''
if (name.includes('bic')) return data.bic || '' if (name.includes('bic')) return data.bic || ''
if (name.includes('bank') || name.includes('kreditinstitut')) return data.bank || '' if (name.includes('bank') || name.includes('kreditinstitut')) return data.bank || ''
if (name.includes('mitglied') || name.includes('mitgliedschaft') || name.includes('art')) return data.mitgliedschaftsart || '' // Do not map generic 'mitglied' to membership type to avoid writing 'aktiv'/'passiv' into text fields.
// Membership selection is handled via checkboxes elsewhere.
if (name.includes('mitgliedschaft') || name.includes('art')) return data.mitgliedschaftsart || ''
return '' return ''
} }
@@ -524,8 +581,21 @@ export default defineEventHandler(async (event) => {
try { try {
// Textfelder // Textfelder
if (typeof field.setText === 'function') { if (typeof field.setText === 'function') {
try {
// don't overwrite if already set
if (typeof field.getText === 'function') {
const cur = field.getText()
if (cur && String(cur).trim() !== '') {
continue
}
}
} catch (e) {
// ignore getter errors and proceed to set
}
const val = mapValue(lower) const val = mapValue(lower)
field.setText(val) if (val != null && String(val).trim() !== '') {
field.setText(val)
}
continue continue
} }
@@ -533,17 +603,31 @@ export default defineEventHandler(async (event) => {
if (typeof field.check === 'function' || typeof field.isChecked === 'function') { if (typeof field.check === 'function' || typeof field.isChecked === 'function') {
// einfache Heuristik: bei Mitgliedschaftsart // einfache Heuristik: bei Mitgliedschaftsart
if (lower.includes('aktiv') || lower.includes('passiv') || lower.includes('mitglied')) { if (lower.includes('aktiv') || lower.includes('passiv') || lower.includes('mitglied')) {
if (data.mitgliedschaftsart && lower.includes(data.mitgliedschaftsart)) { try {
field.check && field.check() if (typeof field.isChecked === 'function' && field.isChecked()) {
} else { // already checked, skip
if (lower.includes('aktiv') && data.mitgliedschaftsart === 'aktiv') field.check && field.check() } else {
if (lower.includes('passiv') && data.mitgliedschaftsart === 'passiv') field.check && field.check() if (data.mitgliedschaftsart && lower.includes(data.mitgliedschaftsart)) {
field.check && field.check()
} else {
if (lower.includes('aktiv') && data.mitgliedschaftsart === 'aktiv') field.check && field.check()
if (lower.includes('passiv') && data.mitgliedschaftsart === 'passiv') field.check && field.check()
}
}
} catch (e) {
// ignore isChecked errors
} }
continue continue
} }
const mapped = mapValue(lower) const mapped = mapValue(lower)
if (mapped === 'true' || mapped === 'ja' || mapped === 'checked') { if (mapped === 'true' || mapped === 'ja' || mapped === 'checked') {
field.check && field.check() try {
if (!(typeof field.isChecked === 'function' && field.isChecked())) {
field.check && field.check()
}
} catch (e) {
field.check && field.check()
}
} }
} }
} catch (e) { } catch (e) {
@@ -551,6 +635,14 @@ export default defineEventHandler(async (event) => {
} }
} }
// Ensure appearances are generated after mapping fields
try {
const helv2 = await pdfDoc.embedFont(StandardFonts.Helvetica)
form.updateFieldAppearances(helv2)
} catch (e) {
console.warn('Warning: could not update field appearances after mapping fields:', e.message)
}
const pdfBytes = await pdfDoc.save() const pdfBytes = await pdfDoc.save()
return Buffer.from(pdfBytes) return Buffer.from(pdfBytes)
} }
@@ -610,9 +702,9 @@ export default defineEventHandler(async (event) => {
// LaTeX-Inhalt generieren // LaTeX-Inhalt generieren
const latexContent = generateLaTeXContent(data) const latexContent = generateLaTeXContent(data)
// LaTeX-Datei schreiben // LaTeX-Datei schreiben
const texPath = path.join(tempDir, `${filename}.tex`) const texPath = path.join(tempDir, `${filename}.tex`)
await fs.writeFile(texPath, latexContent, 'utf8') await fs.writeFile(texPath, latexContent, 'utf8')
// PDF mit pdflatex generieren // PDF mit pdflatex generieren
const command = `cd "${tempDir}" && pdflatex -interaction=nonstopmode "${filename}.tex"` const command = `cd "${tempDir}" && pdflatex -interaction=nonstopmode "${filename}.tex"`