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

@@ -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) {

View File

@@ -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"`