membership: refactor form filling, add smoke tests and debug-guard fallback; fix mappings
This commit is contained in:
@@ -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