diff --git a/pages/mitgliedschaft.vue b/pages/mitgliedschaft.vue index adde999..8cba7a8 100644 --- a/pages/mitgliedschaft.vue +++ b/pages/mitgliedschaft.vue @@ -4,11 +4,66 @@

Mitgliedschaft

-
- +
+
- +
+
+
+

+ Mitgliedschaft +

+
+

+ Werden Sie Teil unserer Tischtennis-Familie - Wählen Sie die passende Mitgliedschaft für sich +

+
+ +
+ +
+ + +
+
+

+ Vereinsatzung +

+

+ Laden Sie unsere aktuelle Vereinsatzung herunter +

+
+ +
+
+
@@ -17,7 +72,7 @@ Beitrittserklärung -
+

@@ -32,13 +87,12 @@

Pflichtfeld

-
- +
- +
-
- +
-
- +
-
@@ -161,21 +212,20 @@

Mitgliedschaftsart

-
-
\ No newline at end of file diff --git a/scripts/create-fillable-template.js b/scripts/create-fillable-template.js index 8539b19..0b79573 100644 --- a/scripts/create-fillable-template.js +++ b/scripts/create-fillable-template.js @@ -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) diff --git a/scripts/fill-sample-template.js b/scripts/fill-sample-template.js index f0239b0..d157a08 100644 --- a/scripts/fill-sample-template.js +++ b/scripts/fill-sample-template.js @@ -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) - // flatten all fields - try { form.flatten() } catch (e) {} + // 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 */ } + } + } + + const out = await pdfDoc.save() if (!fs.existsSync('temp')) fs.mkdirSync('temp') diff --git a/scripts/find-membership-values.js b/scripts/find-membership-values.js new file mode 100644 index 0000000..9748799 --- /dev/null +++ b/scripts/find-membership-values.js @@ -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) }) diff --git a/scripts/inspect-forms.js b/scripts/inspect-forms.js new file mode 100644 index 0000000..d4b0356 --- /dev/null +++ b/scripts/inspect-forms.js @@ -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) }) diff --git a/scripts/smoke-test.js b/scripts/smoke-test.js new file mode 100644 index 0000000..d2809a8 --- /dev/null +++ b/scripts/smoke-test.js @@ -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() diff --git a/server/api/membership/download/[id].get.js b/server/api/membership/download/[id].get.js index 707ba54..1bc16ab 100644 --- a/server/api/membership/download/[id].get.js +++ b/server/api/membership/download/[id].get.js @@ -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) { diff --git a/server/api/membership/generate-pdf.post.js b/server/api/membership/generate-pdf.post.js index 212f4dc..8f0f0ac 100644 --- a/server/api/membership/generate-pdf.post.js +++ b/server/api/membership/generate-pdf.post.js @@ -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"` diff --git a/server/templates/mitgliedschaft-fillable.pdf b/server/templates/mitgliedschaft-fillable.pdf index 2c9bd89..f6ff814 100644 Binary files a/server/templates/mitgliedschaft-fillable.pdf and b/server/templates/mitgliedschaft-fillable.pdf differ