Refactor membership PDF generation logic to improve maintainability and validation; remove deprecated form filling methods and enhance email notification process. Update membership page styles for better layout and user experience.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
150
server/utils/email-service.js
Normal file
150
server/utils/email-service.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Email Service - Handles membership application email notifications
|
||||
* Clean Code: Single Responsibility Principle
|
||||
*/
|
||||
|
||||
import nodemailer from 'nodemailer'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Gets the correct data path for config files
|
||||
* @param {string} filename - Config filename
|
||||
* @returns {string} Full path to config file
|
||||
*/
|
||||
function getDataPath(filename) {
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
if (isProduction) {
|
||||
return path.join(process.cwd(), '..', 'server', 'data', filename)
|
||||
} else {
|
||||
return path.join(process.cwd(), 'server', 'data', filename)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads configuration from config.json
|
||||
* @returns {Promise<Object>} Configuration object
|
||||
*/
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const configPath = getDataPath('config.json')
|
||||
const configData = await fs.readFile(configPath, 'utf8')
|
||||
return JSON.parse(configData)
|
||||
} catch (error) {
|
||||
console.error('Could not load config.json:', error.message)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets email recipients based on membership type and environment
|
||||
* @param {Object} data - Form data
|
||||
* @param {Object} config - Configuration
|
||||
* @returns {Array<string>} Email addresses
|
||||
*/
|
||||
function getEmailRecipients(data, config) {
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
if (!isProduction) {
|
||||
return ['tsschulz@tsschulz.de']
|
||||
}
|
||||
|
||||
const recipients = []
|
||||
|
||||
// Add 1. Vorsitzender
|
||||
if (config.vorsitzender && config.vorsitzender.email) {
|
||||
recipients.push(config.vorsitzender.email)
|
||||
}
|
||||
|
||||
// Add Schriftführer
|
||||
if (config.schriftfuehrer && config.schriftfuehrer.email) {
|
||||
recipients.push(config.schriftfuehrer.email)
|
||||
}
|
||||
|
||||
// For minors, also add 1. Trainer
|
||||
if (!data.isVolljaehrig && config.trainer && config.trainer.email) {
|
||||
recipients.push(config.trainer.email)
|
||||
}
|
||||
|
||||
// Fallback if no recipients found
|
||||
if (recipients.length === 0) {
|
||||
recipients.push('tsschulz@tsschulz.de')
|
||||
}
|
||||
|
||||
return recipients
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates email transporter
|
||||
* @returns {Object} Nodemailer transporter
|
||||
*/
|
||||
function createTransporter() {
|
||||
return nodemailer.createTransporter({
|
||||
host: process.env.SMTP_HOST || 'localhost',
|
||||
port: parseInt(process.env.SMTP_PORT) || 587,
|
||||
secure: process.env.SMTP_SECURE === 'true',
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends membership application email
|
||||
* @param {Object} data - Form data
|
||||
* @param {string} pdfPath - Path to generated PDF
|
||||
* @returns {Promise<Object>} Email result
|
||||
*/
|
||||
export async function sendMembershipEmail(data, pdfPath) {
|
||||
try {
|
||||
const config = await loadConfig()
|
||||
const recipients = getEmailRecipients(data, config)
|
||||
|
||||
// Create transporter
|
||||
const transporter = createTransporter()
|
||||
|
||||
// Email content
|
||||
const subject = `Neuer Mitgliedschaftsantrag - ${data.vorname} ${data.nachname}`
|
||||
const message = `Ein neuer Mitgliedschaftsantrag wurde eingereicht.
|
||||
|
||||
Antragsteller: ${data.vorname} ${data.nachname}
|
||||
Mitgliedschaftsart: ${data.mitgliedschaftsart}
|
||||
Volljährig: ${data.isVolljaehrig ? 'Ja' : 'Nein'}
|
||||
|
||||
Das ausgefüllte Formular ist als Anhang verfügbar.`
|
||||
|
||||
// Email options
|
||||
const mailOptions = {
|
||||
from: process.env.SMTP_FROM || 'noreply@harheimertc.de',
|
||||
to: recipients.join(', '),
|
||||
subject: subject,
|
||||
text: message,
|
||||
attachments: [
|
||||
{
|
||||
filename: path.basename(pdfPath),
|
||||
path: pdfPath
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Send email
|
||||
const info = await transporter.sendMail(mailOptions)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: message,
|
||||
recipients: recipients,
|
||||
messageId: info.messageId
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Email sending failed:', error.message)
|
||||
return {
|
||||
success: false,
|
||||
message: `E-Mail-Versand fehlgeschlagen: ${error.message}`,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
135
server/utils/pdf-field-mapper.js
Normal file
135
server/utils/pdf-field-mapper.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* PDF Field Mapper - Maps form data to PDF field names
|
||||
* Clean Code: Single Responsibility Principle
|
||||
*/
|
||||
|
||||
/**
|
||||
* Formats a date string to DD.MM.YYYY format with leading zeros
|
||||
* @param {string} dateString - Date string (YYYY-MM-DD format)
|
||||
* @returns {string} Formatted date string (DD.MM.YYYY)
|
||||
*/
|
||||
function formatDateWithLeadingZeros(dateString) {
|
||||
if (!dateString) return ''
|
||||
|
||||
try {
|
||||
const date = new Date(dateString)
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const year = date.getFullYear()
|
||||
return `${day}.${month}.${year}`
|
||||
} catch (error) {
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps form data to PDF field values based on field name patterns
|
||||
* @param {Object} data - Form data
|
||||
* @param {string} fieldName - PDF field name
|
||||
* @returns {string} Mapped value
|
||||
*/
|
||||
export function mapFieldValue(data, fieldName) {
|
||||
const name = fieldName.toLowerCase()
|
||||
|
||||
// Specific field mappings
|
||||
const specificMappings = {
|
||||
'sepa_mitglied': () => `${data.vorname || ''} ${data.nachname || ''}`.trim(),
|
||||
'sepa_kontoinhaber': () => data.kontoinhaber || `${data.vorname || ''} ${data.nachname || ''}`.trim(),
|
||||
'sepa_plz_ort': () => `${data.plz || ''} ${data.ort || ''}`.trim(),
|
||||
'page3_anschrift': () => `${data.strasse || ''}, ${data.plz || ''} ${data.ort || ''}`.trim(),
|
||||
'vorname': () => data.vorname || '',
|
||||
'given': () => data.vorname || '',
|
||||
'nachname': () => data.nachname || '',
|
||||
'zuname': () => data.nachname || '',
|
||||
'strasse': () => data.strasse || '',
|
||||
'straße': () => data.strasse || '',
|
||||
'street': () => data.strasse || '',
|
||||
'plz': () => data.plz || '',
|
||||
'ort': () => data.ort || '',
|
||||
'stadt': () => data.ort || '',
|
||||
'plz_ort': () => `${data.plz || ''} ${data.ort || ''}`.trim(),
|
||||
'wohnort': () => `${data.plz || ''} ${data.ort || ''}`.trim(),
|
||||
'geburtsdatum': () => formatDateWithLeadingZeros(data.geburtsdatum),
|
||||
'geb': () => formatDateWithLeadingZeros(data.geburtsdatum),
|
||||
'telefon': () => data.telefon_privat || data.telefon_mobil || '',
|
||||
'tel': () => data.telefon_privat || data.telefon_mobil || '',
|
||||
'email': () => data.email || '',
|
||||
'kontoinhaber': () => data.kontoinhaber || '',
|
||||
'kontoinh': () => data.kontoinhaber || '',
|
||||
'iban': () => data.iban || '',
|
||||
'bic': () => data.bic || '',
|
||||
'bank': () => data.bank || '',
|
||||
'kreditinstitut': () => data.bank || '',
|
||||
'mitgliedschaft': () => data.mitgliedschaftsart || '',
|
||||
'art': () => data.mitgliedschaftsart || ''
|
||||
}
|
||||
|
||||
// Check for specific mappings first
|
||||
for (const [pattern, mapper] of Object.entries(specificMappings)) {
|
||||
if (name.includes(pattern)) {
|
||||
return mapper()
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for PLZ/Ort combinations - more comprehensive
|
||||
if (name.includes('plz') && (name.includes('ort') || name.includes('wohnort') || name.includes('stadt'))) {
|
||||
return `${data.plz || ''} ${data.ort || ''}`.trim()
|
||||
}
|
||||
|
||||
// Handle fields that might contain PLZ/Ort but don't explicitly mention both
|
||||
if (name.includes('plz') && !name.includes('vorname') && !name.includes('nachname') && !name.includes('strasse') && !name.includes('str')) {
|
||||
return `${data.plz || ''} ${data.ort || ''}`.trim()
|
||||
}
|
||||
|
||||
if (name.includes('wohnort') || name.includes('ort') || name.includes('stadt')) {
|
||||
if (!name.includes('vorname') && !name.includes('nachname') && !name.includes('strasse') && !name.includes('str')) {
|
||||
return `${data.plz || ''} ${data.ort || ''}`.trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Generic name mapping (avoid conflicts with specific mappings)
|
||||
if (name.includes('name') && !name.includes('vorname') && !name.includes('given')) {
|
||||
return data.nachname || ''
|
||||
}
|
||||
|
||||
// Date fields
|
||||
if (name.includes('datum')) {
|
||||
return data.sign_datum || data.sepa_datum || data.page3_datum || formatDateWithLeadingZeros(new Date().toISOString().split('T')[0])
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a field should be checked based on membership type
|
||||
* @param {string} fieldName - PDF field name
|
||||
* @param {string} membershipType - Membership type ('aktiv' or 'passiv')
|
||||
* @returns {boolean} Whether the field should be checked
|
||||
*/
|
||||
export function shouldCheckField(fieldName, membershipType) {
|
||||
const name = fieldName.toLowerCase()
|
||||
|
||||
if (name.includes('aktiv') && membershipType === 'aktiv') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (name.includes('passiv') && membershipType === 'passiv') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (name.includes('mitglied') && name.includes(membershipType)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a field should be checked based on boolean value
|
||||
* @param {string} value - Value to check
|
||||
* @returns {boolean} Whether the field should be checked
|
||||
*/
|
||||
export function shouldCheckByValue(value) {
|
||||
const stringValue = String(value).toLowerCase()
|
||||
return stringValue === 'true' || stringValue === 'ja' || stringValue === 'checked'
|
||||
}
|
||||
228
server/utils/pdf-form-filler.js
Normal file
228
server/utils/pdf-form-filler.js
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* PDF Form Filler - Handles filling PDF form fields
|
||||
* Clean Code: Single Responsibility Principle
|
||||
*/
|
||||
|
||||
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'
|
||||
import { mapFieldValue, shouldCheckField, shouldCheckByValue } from './pdf-field-mapper.js'
|
||||
|
||||
/**
|
||||
* Safely sets text field value if it's empty
|
||||
* @param {Object} field - PDF form field
|
||||
* @param {string} value - Value to set
|
||||
*/
|
||||
export function setTextFieldIfEmpty(field, value) {
|
||||
if (typeof field.setText !== 'function') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if field already has content
|
||||
if (typeof field.getText === 'function') {
|
||||
const currentValue = field.getText()
|
||||
if (currentValue && String(currentValue).trim() !== '') {
|
||||
return // Field already has content, don't overwrite
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore getter errors and proceed to set
|
||||
}
|
||||
|
||||
if (value != null && String(value).trim() !== '') {
|
||||
field.setText(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely sets checkbox field based on membership type
|
||||
* @param {Object} field - PDF form field
|
||||
* @param {string} fieldName - Field name
|
||||
* @param {Object} data - Form data
|
||||
*/
|
||||
export function setCheckboxIfNeeded(field, fieldName, data) {
|
||||
if (!(typeof field.check === 'function' || typeof field.isChecked === 'function')) {
|
||||
return
|
||||
}
|
||||
|
||||
const lowerName = fieldName.toLowerCase()
|
||||
|
||||
try {
|
||||
// Handle membership type checkboxes
|
||||
if (lowerName.includes('aktiv') || lowerName.includes('passiv') || lowerName.includes('mitglied')) {
|
||||
if (typeof field.isChecked === 'function' && field.isChecked()) {
|
||||
return // Already checked
|
||||
}
|
||||
|
||||
if (shouldCheckField(lowerName, data.mitgliedschaftsart)) {
|
||||
field.check && field.check()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle other boolean fields
|
||||
const mappedValue = mapFieldValue(data, lowerName)
|
||||
if (shouldCheckByValue(mappedValue)) {
|
||||
try {
|
||||
if (!(typeof field.isChecked === 'function' && field.isChecked())) {
|
||||
field.check && field.check()
|
||||
}
|
||||
} catch (error) {
|
||||
field.check && field.check()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills all form fields in a PDF document
|
||||
* @param {PDFDocument} pdfDoc - PDF document
|
||||
* @param {Object} form - PDF form
|
||||
* @param {Object} data - Form data
|
||||
*/
|
||||
export async function fillFormFields(pdfDoc, form, data) {
|
||||
const fields = form.getFields()
|
||||
|
||||
for (const field of fields) {
|
||||
const fieldName = field.getName()
|
||||
const lowerName = fieldName.toLowerCase()
|
||||
|
||||
// Handle text fields
|
||||
if (typeof field.setText === 'function') {
|
||||
const value = mapFieldValue(data, lowerName)
|
||||
setTextFieldIfEmpty(field, value)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle checkbox fields
|
||||
if (typeof field.check === 'function' || typeof field.isChecked === 'function') {
|
||||
setCheckboxIfNeeded(field, lowerName, data)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Update field appearances
|
||||
try {
|
||||
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)
|
||||
form.updateFieldAppearances(helveticaFont)
|
||||
} catch (error) {
|
||||
console.warn('Could not update field appearances:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills PDF form fields with fallback to positional drawing
|
||||
* @param {PDFDocument} pdfDoc - PDF document
|
||||
* @param {Object} form - PDF form
|
||||
* @param {Object} data - Form data
|
||||
*/
|
||||
export async function fillPdfForm(pdfDoc, form, data) {
|
||||
try {
|
||||
await fillFormFields(pdfDoc, form, data)
|
||||
|
||||
// Check if PLZ/Ort field on page 1 is empty and fix it
|
||||
await fixPLZOrtField(pdfDoc, data)
|
||||
} catch (error) {
|
||||
console.warn('Form filling failed, using fallback:', error.message)
|
||||
await fillFormFieldsPositionally(pdfDoc, data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes the PLZ/Ort field on page 1 if it's empty
|
||||
* @param {PDFDocument} pdfDoc - PDF document
|
||||
* @param {Object} data - Form data
|
||||
*/
|
||||
async function fixPLZOrtField(pdfDoc, data) {
|
||||
try {
|
||||
const pages = pdfDoc.getPages()
|
||||
const firstPage = pages[0]
|
||||
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)
|
||||
|
||||
// Draw PLZ/Ort at the correct position on page 1
|
||||
const plzOrtText = `${data.plz || ''} ${data.ort || ''}`.trim()
|
||||
|
||||
// Instead of drawing rectangles, let's find and fix the actual form field
|
||||
const form = pdfDoc.getForm()
|
||||
const fields = form.getFields()
|
||||
|
||||
// Look for PLZ/Ort related fields and fix them
|
||||
for (const field of fields) {
|
||||
const fieldName = field.getName().toLowerCase()
|
||||
if (fieldName.includes('plz') && fieldName.includes('ort')) {
|
||||
if (typeof field.setText === 'function') {
|
||||
field.setText(plzOrtText)
|
||||
}
|
||||
} else if (fieldName.includes('plz') && !fieldName.includes('vorname') && !fieldName.includes('nachname')) {
|
||||
if (typeof field.setText === 'function') {
|
||||
field.setText(plzOrtText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Could not fix PLZ/Ort field:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: Fill form fields by drawing text at specific positions
|
||||
* @param {PDFDocument} pdfDoc - PDF document
|
||||
* @param {Object} data - Form data
|
||||
*/
|
||||
async function fillFormFieldsPositionally(pdfDoc, data) {
|
||||
try {
|
||||
const pages = pdfDoc.getPages()
|
||||
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)
|
||||
|
||||
// First page coordinates
|
||||
const firstPage = pages[0]
|
||||
const coords = {
|
||||
nachname: { x: 156, y: 160 },
|
||||
vorname: { x: 470, y: 160 },
|
||||
strasse: { x: 156, y: 132 },
|
||||
plz_ort: { x: 470, y: 132 },
|
||||
geburtsdatum: { x: 156, y: 104 },
|
||||
telefon: { x: 470, y: 104 },
|
||||
email: { x: 156, y: 76 },
|
||||
telefon_mobil: { x: 470, y: 76 }
|
||||
}
|
||||
|
||||
// Fill first page fields
|
||||
firstPage.drawText(data.nachname || '', { x: coords.nachname.x, y: coords.nachname.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(data.vorname || '', { x: coords.vorname.x, y: coords.vorname.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(data.strasse || '', { x: coords.strasse.x, y: coords.strasse.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(`${data.plz || ''} ${data.ort || ''}`.trim(), { x: coords.plz_ort.x, y: coords.plz_ort.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(new Date(data.geburtsdatum).toLocaleDateString('de-DE') || '', { x: coords.geburtsdatum.x, y: coords.geburtsdatum.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(data.telefon_privat || data.telefon_mobil || '', { x: coords.telefon.x, y: coords.telefon.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(data.email || '', { x: coords.email.x, y: coords.email.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(data.telefon_mobil || '', { x: coords.telefon_mobil.x, y: coords.telefon_mobil.y, size: 11, font: helveticaFont })
|
||||
|
||||
// Bank data on second page
|
||||
const secondPage = pages[1]
|
||||
if (secondPage) {
|
||||
const bankCoords = {
|
||||
kontoinhaber: { x: 156, y: 400 },
|
||||
iban: { x: 156, y: 370 },
|
||||
bic: { x: 156, y: 340 },
|
||||
bank: { x: 156, y: 310 }
|
||||
}
|
||||
|
||||
secondPage.drawText(data.kontoinhaber || '', { x: bankCoords.kontoinhaber.x, y: bankCoords.kontoinhaber.y, size: 11, font: helveticaFont })
|
||||
secondPage.drawText(data.iban || '', { x: bankCoords.iban.x, y: bankCoords.iban.y, size: 11, font: helveticaFont })
|
||||
secondPage.drawText(data.bic || '', { x: bankCoords.bic.x, y: bankCoords.bic.y, size: 11, font: helveticaFont })
|
||||
secondPage.drawText(data.bank || '', { x: bankCoords.bank.x, y: bankCoords.bank.y, size: 11, font: helveticaFont })
|
||||
}
|
||||
|
||||
// Membership checkbox
|
||||
if (data.mitgliedschaftsart === 'aktiv') {
|
||||
firstPage.drawText('X', { x: 116, y: 20, size: 12, font: helveticaFont })
|
||||
} else if (data.mitgliedschaftsart === 'passiv') {
|
||||
firstPage.drawText('X', { x: 116, y: -8, size: 12, font: helveticaFont })
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Positional filling failed:', error.message)
|
||||
}
|
||||
}
|
||||
105
server/utils/pdf-generator-service.js
Normal file
105
server/utils/pdf-generator-service.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* PDF Generator Service - Main service for PDF generation
|
||||
* Clean Code: Facade Pattern, Single Responsibility
|
||||
*/
|
||||
|
||||
import { PDFDocument } from 'pdf-lib'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { fillPdfForm } from './pdf-form-filler.js'
|
||||
|
||||
/**
|
||||
* PDF Generation Result
|
||||
*/
|
||||
export class PDFGenerationResult {
|
||||
constructor(success, pdfBuffer, filename, error = null) {
|
||||
this.success = success
|
||||
this.pdfBuffer = pdfBuffer
|
||||
this.filename = filename
|
||||
this.error = error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PDF Generator Service
|
||||
*/
|
||||
export class PDFGeneratorService {
|
||||
constructor() {
|
||||
this.templatePath = path.join(process.cwd(), 'server', 'templates', 'mitgliedschaft-fillable.pdf')
|
||||
this.fallbackTemplatePath = path.join(process.cwd(), 'server', 'templates', 'Aufnahmeantrag 2025.pdf')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates PDF from template with form data
|
||||
* @param {Object} data - Form data
|
||||
* @returns {Promise<PDFGenerationResult>} Generation result
|
||||
*/
|
||||
async generateFromTemplate(data) {
|
||||
try {
|
||||
const templatePath = await this.getTemplatePath()
|
||||
const templateBytes = await fs.readFile(templatePath)
|
||||
const pdfDoc = await PDFDocument.load(templateBytes)
|
||||
const form = pdfDoc.getForm()
|
||||
|
||||
// Fill form fields
|
||||
await fillPdfForm(pdfDoc, form, data)
|
||||
|
||||
// Don't flatten form to keep fields editable
|
||||
// form.flatten() makes fields non-editable
|
||||
|
||||
// Generate filename
|
||||
const filename = this.generateFilename(data)
|
||||
|
||||
// Save PDF
|
||||
const pdfBytes = await pdfDoc.save()
|
||||
|
||||
return new PDFGenerationResult(true, Buffer.from(pdfBytes), filename)
|
||||
} catch (error) {
|
||||
console.error('Template PDF generation failed:', error.message)
|
||||
return new PDFGenerationResult(false, null, null, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the appropriate template path
|
||||
* @returns {Promise<string>} Template path
|
||||
*/
|
||||
async getTemplatePath() {
|
||||
try {
|
||||
await fs.access(this.templatePath)
|
||||
return this.templatePath
|
||||
} catch (error) {
|
||||
try {
|
||||
await fs.access(this.fallbackTemplatePath)
|
||||
return this.fallbackTemplatePath
|
||||
} catch (fallbackError) {
|
||||
throw new Error('No PDF template found')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates filename for PDF
|
||||
* @param {Object} data - Form data
|
||||
* @returns {string} Filename
|
||||
*/
|
||||
generateFilename(data) {
|
||||
const timestamp = Date.now()
|
||||
const name = `${data.nachname || 'Unbekannt'}_${data.vorname || 'Unbekannt'}`
|
||||
return `beitrittserklärung_${timestamp}.pdf`
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves PDF to file system
|
||||
* @param {Buffer} pdfBuffer - PDF buffer
|
||||
* @param {string} filename - Filename
|
||||
* @param {string} uploadDir - Upload directory
|
||||
* @returns {Promise<string>} File path
|
||||
*/
|
||||
async savePDF(pdfBuffer, filename, uploadDir) {
|
||||
const filePath = path.join(uploadDir, filename)
|
||||
await fs.writeFile(filePath, pdfBuffer)
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user