membership: refactor form filling, add smoke tests and debug-guard fallback; fix mappings
This commit is contained in:
@@ -4,11 +4,66 @@
|
||||
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-6">
|
||||
Mitgliedschaft
|
||||
</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) -->
|
||||
<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>
|
||||
|
||||
<!-- Aufnahmeantrag Formular -->
|
||||
@@ -17,7 +72,7 @@
|
||||
Beitrittserklärung
|
||||
</h2>
|
||||
|
||||
<form @submit.prevent="submitForm" class="space-y-8">
|
||||
<form id="membershipForm" class="space-y-8">
|
||||
<!-- Persönliche Daten -->
|
||||
<div class="space-y-6">
|
||||
<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>
|
||||
<input
|
||||
id="nachname"
|
||||
v-model="form.nachname"
|
||||
name="nachname"
|
||||
type="text"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="vorname" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Vorname
|
||||
@@ -46,7 +100,7 @@
|
||||
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
|
||||
<input
|
||||
id="vorname"
|
||||
v-model="form.vorname"
|
||||
name="vorname"
|
||||
type="text"
|
||||
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"
|
||||
@@ -61,7 +115,7 @@
|
||||
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
|
||||
<input
|
||||
id="strasse"
|
||||
v-model="form.strasse"
|
||||
name="strasse"
|
||||
type="text"
|
||||
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"
|
||||
@@ -76,14 +130,13 @@
|
||||
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
|
||||
<input
|
||||
id="plz"
|
||||
v-model="form.plz"
|
||||
name="plz"
|
||||
type="text"
|
||||
required
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="ort" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Wohnort
|
||||
@@ -91,7 +144,7 @@
|
||||
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
|
||||
<input
|
||||
id="ort"
|
||||
v-model="form.ort"
|
||||
name="ort"
|
||||
type="text"
|
||||
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"
|
||||
@@ -107,20 +160,19 @@
|
||||
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
|
||||
<input
|
||||
id="geburtsdatum"
|
||||
v-model="form.geburtsdatum"
|
||||
name="geburtsdatum"
|
||||
type="date"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="telefon_privat" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Telefon (privat)
|
||||
</label>
|
||||
<input
|
||||
id="telefon_privat"
|
||||
v-model="form.telefon_privat"
|
||||
name="telefon_privat"
|
||||
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"
|
||||
/>
|
||||
@@ -135,20 +187,19 @@
|
||||
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
name="email"
|
||||
type="email"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="telefon_mobil" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Telefon (Mobil)
|
||||
</label>
|
||||
<input
|
||||
id="telefon_mobil"
|
||||
v-model="form.telefon_mobil"
|
||||
name="telefon_mobil"
|
||||
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"
|
||||
/>
|
||||
@@ -161,21 +212,20 @@
|
||||
<h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2">
|
||||
Mitgliedschaftsart
|
||||
</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
v-model="form.mitgliedschaftsart"
|
||||
name="mitgliedschaftsart"
|
||||
type="radio"
|
||||
value="aktiv"
|
||||
checked
|
||||
class="mr-3 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
<span class="text-gray-700">Aktives Mitglied</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
v-model="form.mitgliedschaftsart"
|
||||
name="mitgliedschaftsart"
|
||||
type="radio"
|
||||
value="passiv"
|
||||
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">
|
||||
Beitragszahlung
|
||||
</h3>
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<p class="text-gray-700 mb-4">
|
||||
Den derzeitigen jährlichen Mitgliedsbeitrag in Höhe von:
|
||||
@@ -204,10 +253,9 @@
|
||||
bitte ich per Lastschrift jährlich von meinem Konto einzuziehen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label class="flex items-start">
|
||||
<input
|
||||
v-model="form.lastschrift_erlaubt"
|
||||
name="lastschrift_erlaubt"
|
||||
type="checkbox"
|
||||
required
|
||||
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>
|
||||
<input
|
||||
id="kontoinhaber"
|
||||
v-model="form.kontoinhaber"
|
||||
name="kontoinhaber"
|
||||
type="text"
|
||||
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"
|
||||
@@ -248,7 +296,7 @@
|
||||
<p class="text-xs text-gray-500 italic mb-2">Pflichtfeld</p>
|
||||
<input
|
||||
id="iban"
|
||||
v-model="form.iban"
|
||||
name="iban"
|
||||
type="text"
|
||||
required
|
||||
placeholder="DE89 3704 0044 0532 0130 00"
|
||||
@@ -262,7 +310,7 @@
|
||||
</label>
|
||||
<input
|
||||
id="bic"
|
||||
v-model="form.bic"
|
||||
name="bic"
|
||||
type="text"
|
||||
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"
|
||||
@@ -275,7 +323,7 @@
|
||||
</label>
|
||||
<input
|
||||
id="bank"
|
||||
v-model="form.bank"
|
||||
name="bank"
|
||||
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"
|
||||
/>
|
||||
@@ -287,7 +335,6 @@
|
||||
<h3 class="text-xl font-semibold text-gray-900 border-b border-gray-200 pb-2">
|
||||
Datenschutz und Einverständniserklärung
|
||||
</h3>
|
||||
|
||||
<div class="bg-blue-50 p-4 rounded-lg">
|
||||
<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.
|
||||
@@ -295,7 +342,7 @@
|
||||
|
||||
<label class="flex items-start">
|
||||
<input
|
||||
v-model="form.datenschutz_einverstanden"
|
||||
name="datenschutz_einverstanden"
|
||||
type="checkbox"
|
||||
required
|
||||
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">
|
||||
Vereinssatzung
|
||||
</h3>
|
||||
|
||||
<label class="flex items-start">
|
||||
<input
|
||||
v-model="form.satzung_anerkannt"
|
||||
name="satzung_anerkannt"
|
||||
type="checkbox"
|
||||
required
|
||||
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">
|
||||
<button
|
||||
type="button"
|
||||
@click="fillWithDummyData"
|
||||
:disabled="isGenerating"
|
||||
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"
|
||||
id="fillDummyData"
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
@@ -358,14 +403,14 @@
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="!isFormValid || isGenerating"
|
||||
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"
|
||||
id="submitBtn"
|
||||
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">
|
||||
<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>
|
||||
</svg>
|
||||
{{ isGenerating ? 'Formular wird erstellt...' : 'Beitrittsformular erstellen' }}
|
||||
Beitrittsformular erstellen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -379,184 +424,97 @@
|
||||
<p class="text-xl text-primary-100 mb-6">
|
||||
Kontaktieren Sie uns - wir beraten Sie gerne persönlich
|
||||
</p>
|
||||
<NuxtLink
|
||||
to="/kontakt"
|
||||
<a
|
||||
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"
|
||||
>
|
||||
Jetzt Kontakt aufnehmen
|
||||
</NuxtLink>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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({
|
||||
// Persönliche Daten
|
||||
nachname: '',
|
||||
vorname: '',
|
||||
strasse: '',
|
||||
plz: '',
|
||||
ort: '',
|
||||
geburtsdatum: '',
|
||||
telefon_privat: '',
|
||||
email: '',
|
||||
telefon_mobil: '',
|
||||
// Testdaten füllen
|
||||
fillDummyBtn.addEventListener('click', () => {
|
||||
document.getElementById('nachname').value = 'Mustermann'
|
||||
document.getElementById('vorname').value = 'Max'
|
||||
document.getElementById('strasse').value = 'Musterstraße 123'
|
||||
document.getElementById('plz').value = '60437'
|
||||
document.getElementById('ort').value = 'Frankfurt am Main'
|
||||
document.getElementById('geburtsdatum').value = '1990-05-15'
|
||||
document.getElementById('telefon_privat').value = '069 12345678'
|
||||
document.getElementById('email').value = 'max.mustermann@example.com'
|
||||
document.getElementById('telefon_mobil').value = '0171 1234567'
|
||||
document.getElementById('kontoinhaber').value = 'Max Mustermann'
|
||||
document.getElementById('iban').value = 'DE89 3704 0044 0532 0130 00'
|
||||
document.getElementById('bic').value = 'COBADEFFXXX'
|
||||
document.getElementById('bank').value = 'Commerzbank AG'
|
||||
document.querySelector('input[name="lastschrift_erlaubt"]').checked = true
|
||||
document.querySelector('input[name="datenschutz_einverstanden"]').checked = true
|
||||
document.querySelector('input[name="satzung_anerkannt"]').checked = true
|
||||
})
|
||||
|
||||
// Mitgliedschaftsart
|
||||
mitgliedschaftsart: 'aktiv',
|
||||
// Formular absenden
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
// Bankdaten
|
||||
kontoinhaber: '',
|
||||
iban: '',
|
||||
bic: '',
|
||||
bank: '',
|
||||
submitBtn.disabled = true
|
||||
submitBtn.textContent = 'Formular wird erstellt...'
|
||||
|
||||
// Einverständnisse
|
||||
lastschrift_erlaubt: false,
|
||||
datenschutz_einverstanden: false,
|
||||
satzung_anerkannt: false
|
||||
})
|
||||
try {
|
||||
const formData = new FormData(form)
|
||||
const data = Object.fromEntries(formData.entries())
|
||||
|
||||
const isGenerating = ref(false)
|
||||
// Volljährigkeit prüfen
|
||||
const birthDate = new Date(data.geburtsdatum)
|
||||
const today = new Date()
|
||||
const age = today.getFullYear() - birthDate.getFullYear()
|
||||
const monthDiff = today.getMonth() - birthDate.getMonth()
|
||||
data.isVolljaehrig = age > 18 || (age === 18 && monthDiff >= 0)
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
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 response = await fetch('/api/membership/generate-pdf', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
const isVolljaehrig = computed(() => {
|
||||
if (!form.value.geburtsdatum) return true // Default zu volljährig
|
||||
const result = await response.json()
|
||||
|
||||
const heute = new Date()
|
||||
const geburtsdatum = new Date(form.value.geburtsdatum)
|
||||
const alter = heute.getFullYear() - geburtsdatum.getFullYear()
|
||||
const monatDiff = heute.getMonth() - geburtsdatum.getMonth()
|
||||
if (result.success) {
|
||||
// PDF herunterladen
|
||||
const downloadResponse = await fetch(result.downloadUrl)
|
||||
const blob = await downloadResponse.blob()
|
||||
|
||||
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
|
||||
mitgliedschaftsart: 'aktiv',
|
||||
|
||||
// Bankdaten
|
||||
kontoinhaber: 'Max Mustermann',
|
||||
iban: 'DE89 3704 0044 0532 0130 00',
|
||||
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)
|
||||
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)
|
||||
|
||||
// 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.')
|
||||
alert('Beitrittsformular erfolgreich erstellt und heruntergeladen!')
|
||||
} else {
|
||||
alert('Fehler beim Erstellen des Formulars: ' + (result.error || 'Unbekannter Fehler'))
|
||||
}
|
||||
|
||||
// Formular zurücksetzen
|
||||
form.value = {
|
||||
nachname: '',
|
||||
vorname: '',
|
||||
strasse: '',
|
||||
plz: '',
|
||||
ort: '',
|
||||
geburtsdatum: '',
|
||||
telefon_privat: '',
|
||||
email: '',
|
||||
telefon_mobil: '',
|
||||
mitgliedschaftsart: 'aktiv',
|
||||
kontoinhaber: '',
|
||||
iban: '',
|
||||
bic: '',
|
||||
bank: '',
|
||||
lastschrift_erlaubt: false,
|
||||
datenschutz_einverstanden: false,
|
||||
satzung_anerkannt: false
|
||||
}
|
||||
} else {
|
||||
alert('Fehler beim Erstellen des Formulars: ' + (response.error || 'Unbekannter Fehler'))
|
||||
} catch (error) {
|
||||
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>
|
||||
@@ -507,11 +507,19 @@ Das Vereinsmitglied trifft die Entscheidung zur Veröffentlichung seiner Daten i
|
||||
const sigLabelSize = 10
|
||||
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()
|
||||
fs.writeFileSync('server/templates/mitgliedschaft-fillable.pdf', pdfBytes)
|
||||
console.log('Wrote server/templates/mitgliedschaft-fillable.pdf')
|
||||
}
|
||||
|
||||
|
||||
create().catch(e => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
|
||||
@@ -12,6 +12,9 @@ async function fill() {
|
||||
const pdfDoc = await PDFDocument.load(existingPdfBytes)
|
||||
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
|
||||
const sample = {
|
||||
nachname: 'Müller',
|
||||
@@ -51,45 +54,200 @@ async function fill() {
|
||||
}
|
||||
}
|
||||
|
||||
safeSetText('nachname', sample.nachname)
|
||||
safeSetText('vorname', sample.vorname)
|
||||
safeSetText('strasse', sample.strasse)
|
||||
safeSetText('plz_ort', sample.plz_ort)
|
||||
safeSetText('geburtsdatum', sample.geburtsdatum)
|
||||
safeSetText('telefon', sample.telefon)
|
||||
safeSetText('email', sample.email)
|
||||
safeSetText('telefon_mobil', sample.telefon_mobil)
|
||||
// Robust setter: find a field by name (case-insensitive) and set text/checkbox/select accordingly
|
||||
function setFieldByName(name, value) {
|
||||
try {
|
||||
const lower = name.toLowerCase()
|
||||
const field = form.getFields().find(f => f.getName() && f.getName().toLowerCase() === lower)
|
||||
if (!field) {
|
||||
console.log(`DEBUG: Field not found for '${name}'`)
|
||||
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 {
|
||||
const cbA = form.getCheckBox('mitglied_aktiv')
|
||||
if (sample.mitglied_aktiv) cbA.check(); else cbA.uncheck()
|
||||
} catch(e) {}
|
||||
try {
|
||||
const cbP = form.getCheckBox('mitglied_passiv')
|
||||
if (sample.mitglied_passiv) cbP.check(); else cbP.uncheck()
|
||||
} catch(e) {}
|
||||
const allFields = form.getFields().map(f => f.getName())
|
||||
console.log('DEBUG: Template form fields:', allFields.join(', '))
|
||||
} catch (e) {
|
||||
console.log('DEBUG: Could not list form fields:', e.message)
|
||||
}
|
||||
|
||||
safeSetText('sepa_mitglied', sample.sepa_mitglied)
|
||||
safeSetText('sepa_kontoinhaber', sample.sepa_kontoinhaber)
|
||||
safeSetText('sepa_strasse', sample.sepa_strasse)
|
||||
safeSetText('sepa_plz_ort', sample.sepa_plz_ort)
|
||||
safeSetText('sepa_bank', sample.sepa_bank)
|
||||
safeSetText('sepa_iban', sample.sepa_iban)
|
||||
safeSetText('sepa_bic', sample.sepa_bic)
|
||||
safeSetText('sepa_datum', sample.sepa_datum)
|
||||
safeSetText('sign_datum', sample.sign_datum)
|
||||
setFieldByName('nachname', sample.nachname)
|
||||
setFieldByName('vorname', sample.vorname)
|
||||
setFieldByName('strasse', sample.strasse)
|
||||
setFieldByName('plz_ort', sample.plz_ort)
|
||||
setFieldByName('geburtsdatum', sample.geburtsdatum)
|
||||
setFieldByName('telefon', sample.telefon)
|
||||
setFieldByName('email', sample.email)
|
||||
setFieldByName('telefon_mobil', sample.telefon_mobil)
|
||||
|
||||
// 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
|
||||
safeSetText('page3_name', sample.page3_name)
|
||||
safeSetText('page3_vorname', sample.page3_vorname)
|
||||
safeSetText('page3_anschrift', sample.page3_anschrift)
|
||||
safeSetText('page3_telefon', sample.page3_telefon)
|
||||
safeSetText('page3_fax', sample.page3_fax)
|
||||
safeSetText('page3_email', sample.page3_email)
|
||||
safeSetText('page3_datum', sample.page3_datum)
|
||||
setFieldByName('page3_name', sample.page3_name)
|
||||
setFieldByName('page3_vorname', sample.page3_vorname)
|
||||
setFieldByName('page3_anschrift', sample.page3_anschrift)
|
||||
setFieldByName('page3_telefon', sample.page3_telefon)
|
||||
setFieldByName('page3_fax', sample.page3_fax)
|
||||
setFieldByName('page3_email', sample.page3_email)
|
||||
setFieldByName('page3_datum', sample.page3_datum)
|
||||
|
||||
// Debug: check which sample keys correspond to actual fields
|
||||
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 */ }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// flatten all fields
|
||||
try { form.flatten() } catch (e) {}
|
||||
|
||||
const out = await pdfDoc.save()
|
||||
if (!fs.existsSync('temp')) fs.mkdirSync('temp')
|
||||
|
||||
40
scripts/find-membership-values.js
Normal file
40
scripts/find-membership-values.js
Normal 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
77
scripts/inspect-forms.js
Normal 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
25
scripts/smoke-test.js
Normal 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()
|
||||
@@ -45,13 +45,22 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfen ob es sich um eine aktuelle Session handelt (innerhalb der letzten 30 Minuten)
|
||||
const sessionKey = `download_${fileId}`
|
||||
const sessionValue = getCookie(event, sessionKey)
|
||||
// Prüfen ob es sich um eine aktuelle Session handelt (innerhalb der letzten 24 Stunden)
|
||||
const downloadToken = getCookie(event, 'download_token')
|
||||
|
||||
if (sessionValue === 'authorized') {
|
||||
// Session-basierte Berechtigung für Antragsteller
|
||||
isAuthorized = true
|
||||
if (downloadToken) {
|
||||
try {
|
||||
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) {
|
||||
|
||||
@@ -9,6 +9,81 @@ import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'
|
||||
const require = createRequire(import.meta.url)
|
||||
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) {
|
||||
const heute = new Date().toLocaleDateString('de-DE')
|
||||
|
||||
@@ -386,11 +461,11 @@ export default defineEventHandler(async (event) => {
|
||||
// Membership checkbox positions (approx.)
|
||||
mitglied_checkbox_aktiv: { x: leftX - 40, y: baseY - gap * 6 + yOffset },
|
||||
mitglied_checkbox_passiv: { x: leftX - 40, y: baseY - gap * 7 + yOffset },
|
||||
// Account details on subsequent page(s)
|
||||
kontoinhaber: { x: leftX, y: baseY - gap * 12 + yOffset },
|
||||
iban: { x: leftX, y: baseY - gap * 13 + yOffset },
|
||||
bic: { x: leftX, y: baseY - gap * 14 + yOffset },
|
||||
bank: { x: leftX, y: baseY - gap * 15 + yOffset }
|
||||
// Account details on subsequent page(s) - korrigierte Koordinaten für Seite 2
|
||||
kontoinhaber: { x: leftX, y: baseY + yOffset },
|
||||
iban: { x: leftX, y: baseY - gap + yOffset },
|
||||
bic: { x: leftX, y: baseY - gap * 2 + yOffset },
|
||||
bank: { x: leftX, y: baseY - gap * 3 + yOffset }
|
||||
}
|
||||
|
||||
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.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 })
|
||||
// Kontodaten evtl. auf andere Seite: falls mehrere Seiten vorhanden, nutze last page
|
||||
const lastPage = pages[pages.length - 1]
|
||||
lastPage.drawText(data.kontoinhaber || '', { x: coords.kontoinhaber.x, y: coords.kontoinhaber.y, size: 11, font: helveticaFont })
|
||||
lastPage.drawText(data.iban || '', { x: coords.iban.x, y: coords.iban.y, size: 11, font: helveticaFont })
|
||||
lastPage.drawText(data.bic || '', { x: coords.bic.x, y: coords.bic.y, size: 11, font: helveticaFont })
|
||||
lastPage.drawText(data.bank || '', { x: coords.bank.x, y: coords.bank.y, size: 11, font: helveticaFont })
|
||||
// Bankdaten als sichtbarer Text am Ende der ersten Seite (garantiert sichtbar)
|
||||
const bottomY = 50 // Am unteren Rand der Seite
|
||||
firstPage.drawText(`BANKDATEN (SEITE 2):`, { x: 50, y: bottomY + 100, size: 12, font: helveticaFont, color: rgb(1, 0, 0) })
|
||||
firstPage.drawText(`Kontoinhaber: ${data.kontoinhaber || ''}`, { x: 50, y: bottomY + 80, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(`IBAN: ${data.iban || ''}`, { x: 50, y: bottomY + 60, 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
|
||||
try {
|
||||
if (data.mitgliedschaftsart === 'aktiv') {
|
||||
@@ -469,39 +545,16 @@ export default defineEventHandler(async (event) => {
|
||||
// Wenn Formularfelder existieren: befülle sie per AcroForm
|
||||
const fields = form.getFields()
|
||||
if (fields && fields.length > 0) {
|
||||
try {
|
||||
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)
|
||||
}
|
||||
try { await fillFormFields(pdfDoc, form, data) } catch (e) { console.warn('AcroForm-Füllung fehlgeschlagen, fallback auf positional:', e.message) }
|
||||
}
|
||||
const mapValue = (name) => {
|
||||
// einfache Heuristiken für Feldnamen
|
||||
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('vorname') || name.includes('given')) return data.vorname || ''
|
||||
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('telefon') || name.includes('tel')) return data.telefon_privat || data.telefon_mobil || ''
|
||||
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('iban')) return data.iban || ''
|
||||
if (name.includes('bic')) return data.bic || ''
|
||||
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 ''
|
||||
}
|
||||
|
||||
@@ -524,8 +581,21 @@ export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Textfelder
|
||||
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)
|
||||
field.setText(val)
|
||||
if (val != null && String(val).trim() !== '') {
|
||||
field.setText(val)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -533,17 +603,31 @@ export default defineEventHandler(async (event) => {
|
||||
if (typeof field.check === 'function' || typeof field.isChecked === 'function') {
|
||||
// einfache Heuristik: bei Mitgliedschaftsart
|
||||
if (lower.includes('aktiv') || lower.includes('passiv') || lower.includes('mitglied')) {
|
||||
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()
|
||||
try {
|
||||
if (typeof field.isChecked === 'function' && field.isChecked()) {
|
||||
// already checked, skip
|
||||
} else {
|
||||
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
|
||||
}
|
||||
const mapped = mapValue(lower)
|
||||
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) {
|
||||
@@ -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()
|
||||
return Buffer.from(pdfBytes)
|
||||
}
|
||||
@@ -610,9 +702,9 @@ export default defineEventHandler(async (event) => {
|
||||
// LaTeX-Inhalt generieren
|
||||
const latexContent = generateLaTeXContent(data)
|
||||
|
||||
// LaTeX-Datei schreiben
|
||||
const texPath = path.join(tempDir, `${filename}.tex`)
|
||||
await fs.writeFile(texPath, latexContent, 'utf8')
|
||||
// LaTeX-Datei schreiben
|
||||
const texPath = path.join(tempDir, `${filename}.tex`)
|
||||
await fs.writeFile(texPath, latexContent, 'utf8')
|
||||
|
||||
// PDF mit pdflatex generieren
|
||||
const command = `cd "${tempDir}" && pdflatex -interaction=nonstopmode "${filename}.tex"`
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user