Files
harheimertc/scripts/create-fillable-template.js

358 lines
20 KiB
JavaScript

import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'
import fs from 'fs'
async function create() {
const pdfDoc = await PDFDocument.create()
const page = pdfDoc.addPage([595.28, 841.89]) // A4
const helv = await pdfDoc.embedFont(StandardFonts.Helvetica)
const helvBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold)
const { width, height } = page.getSize()
// left column moved further left to align with checkboxes
const leftX = 48
const rightX = 320
// baseY is the vertical anchor for page-1 content. Increase it by ~5.2mm (≈14.739pt)
const baseY = height - 160 + 14.739
// shift to apply only to labels and input fields (move those 1cm down)
const labelsShift = -28.35
const gap = 36
const labelWidth = 100
const fieldXOffset = 100
// Make fields 10% narrower
const fieldWidth = Math.round(180 * 0.9)
// shrink most fields by another 20% on request (applied selectively later)
const fieldShrinkFactor = 0.8
const fieldHeight = 14
// raise date fields by 0.4cm (≈11.34pt) when requested; add extra 1.2mm (≈3.4016pt) per user request
const dateRaise = 11.34 + 3.4016 // now ≈14.7416pt
// fixed checkbox positions (do not move with leftX)
const cbX = 48
const cbYOffset = 5
// Header: centered club name and a full-width horizontal bar underneath (~2mm high)
const headerText = 'Harheimer Tischtennis-Club 1954 e.V.'
const headerSize = 20
const textWidth = helv.widthOfTextAtSize(headerText, headerSize)
const headerX = (width - textWidth) / 2
const headerY = height - 72
page.drawText(headerText, { x: headerX, y: headerY, size: headerSize, font: helv })
// draw full-width bar directly under the header; 2mm ≈ 5.67 points
const barHeight = 5.67
const barY = headerY - 20
page.drawRectangle({ x: 0, y: barY, width: width, height: barHeight, color: rgb(0,0,0) })
// Labels and lines
// Labels left-aligned in their columns
// Add form title above the fields
const titleText = 'Beitrittserklärung'
const titleSize = 14
const titleWidth = helvBold.widthOfTextAtSize(titleText, titleSize)
// left-align title above the left labels and move up ~0.7cm (≈20pt)
const titleX = leftX
// move title down by 0.5cm (≈14.17pt)
const titleY = baseY + 24 + 20 - 14.17
page.drawText(titleText, { x: titleX, y: titleY, size: titleSize, font: helvBold })
const subtitle = 'Hiermit beantrage ich,'
const subtitleSize = 12
const subtitleX = leftX
const subtitleY = titleY - 18 - 14.17
page.drawText(subtitle, { x: subtitleX, y: subtitleY, size: subtitleSize, font: helv })
// apply same vertical shift as fields so labels align
const fieldsShift = -14.17
page.drawText('Name:', { x: leftX, y: baseY + fieldsShift + labelsShift, size: 12, font: helv })
page.drawText('Vorname:', { x: rightX, y: baseY + fieldsShift + labelsShift, size: 12, font: helv })
page.drawText('Straße:', { x: leftX, y: baseY - gap + fieldsShift + labelsShift, size: 12, font: helv })
page.drawText('PLZ/Wohnort:', { x: rightX, y: baseY - gap + fieldsShift + labelsShift, size: 12, font: helv })
page.drawText('Geburtsdatum:', { x: leftX, y: baseY - gap * 2 + fieldsShift + labelsShift, size: 12, font: helv })
page.drawText('Telefon(privat):', { x: rightX, y: baseY - gap * 2 + fieldsShift + labelsShift, size: 12, font: helv })
page.drawText('E-Mail:', { x: leftX, y: baseY - gap * 3 + fieldsShift + labelsShift, size: 12, font: helv })
page.drawText('Telefon(Mobil):', { x: rightX, y: baseY - gap * 3 + fieldsShift + labelsShift, size: 12, font: helv })
// Create form fields
const form = pdfDoc.getForm()
// Place fields on the same baseline as their labels
// We need to move only the input fields on page 1 up by 0.6cm (≈17.01pt) without moving labels.
const labelToFieldYDelta = 2 // small vertical offset so field baseline matches label visually
const lift = 0 // original lift value
// previously raised inputs by 17.01pt (0.6cm); move them down by 5.67pt (0.2cm)
const inputFieldRaise = 11.34 // net upward offset now ~11.34pt
form.createTextField('nachname').addToPage(page, { x: leftX + fieldXOffset, y: baseY - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv })
form.createTextField('vorname').addToPage(page, { x: rightX + fieldXOffset, y: baseY - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv })
form.createTextField('strasse').addToPage(page, { x: leftX + fieldXOffset, y: baseY - gap - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv })
form.createTextField('plz_ort').addToPage(page, { x: rightX + fieldXOffset, y: baseY - gap - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv })
form.createTextField('geburtsdatum').addToPage(page, { x: leftX + fieldXOffset, y: baseY - gap * 2 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv })
form.createTextField('telefon').addToPage(page, { x: rightX + fieldXOffset, y: baseY - gap * 2 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv })
form.createTextField('email').addToPage(page, { x: leftX + fieldXOffset, y: baseY - gap * 3 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv })
form.createTextField('telefon_mobil').addToPage(page, { x: rightX + fieldXOffset, y: baseY - gap * 3 - fieldHeight + labelToFieldYDelta + lift + fieldsShift + labelsShift + inputFieldRaise, width: Math.round(fieldWidth * fieldShrinkFactor), height: fieldHeight, font: helv })
// read membership amounts from config (fall back to defaults)
let erw = 120, jug = 72, passv = 30
try {
const cfg = JSON.parse(fs.readFileSync('server/data/config.json', 'utf8'))
const ms = cfg.mitgliedschaft || []
erw = (ms.find(m => /Erwachsene/i.test(m.typ)) || ms.find(m => m.typ === 'Erwachsene') || {}).preis || erw
jug = (ms.find(m => /Jugend|Kinder/i.test(m.typ)) || ms.find(m => m.typ === 'Kinder/Jugend') || {}).preis || jug
passv = (ms.find(m => /Passiv/i.test(m.typ)) || {}).preis || passv
} catch (e) {
console.error('Could not read config for membership amounts', e)
}
const paraLines = [
'Den derzeitigen jährlichen Mitgliedsbeitrag in Höhe von',
`${erw},-- (Erwachsene)`,
`${jug},-- (Jugendliche bis zum vollendeten 18. Lebensjahr)`,
`${passv},-- (passive Mitglieder)`,
'bitte ich per Lastschrift jährlich von meinem Konto einzuziehen.',
'Hierzu erteile ich beigefügtes SEPA-Lastschriftmandat.',
'Mir ist bekannt, dass die Mitgliedschaft im Harheimer Tischtennis-Club erst nach Bestätigung durch den',
'Vorstand Wirksamkeit erlangt. Die Beitragspflicht beginnt mit dem darauf folgenden Monat.',
'Ich erkenne die Vereinssatzung (erhältlich beim Vorstand bzw. auf der Vereinshomepage) an.'
]
// compute intermediate line position and move it down by additional 0.3cm ≈ 8.5pt
const intermediateY = baseY - gap * 3 + fieldsShift - 6 - 14.17 - 8.5 - 8.5 + labelsShift
page.drawText('meine Aufnahme in den Harheimer Tischtennis-Club 1954 e.V. als', { x: leftX, y: intermediateY, size: 12, font: helv })
// membership checkboxes
// Keep checkbox labels and boxes at fixed left positions, but move them down by 0.8cm ≈ 22.68pt
const cbLift = -22.68
page.drawText('aktives Mitglied', { x: cbX + 16, y: baseY - gap * 5 + cbLift + labelsShift, size: 12, font: helv })
page.drawText('passives Mitglied', { x: cbX + 16, y: baseY - gap * 6 + cbLift + labelsShift, size: 12, font: helv })
form.createCheckBox('mitglied_aktiv').addToPage(page, { x: cbX, y: baseY - gap * 5 - cbYOffset + cbLift + labelsShift, width: 12, height: 12 })
form.createCheckBox('mitglied_passiv').addToPage(page, { x: cbX, y: baseY - gap * 6 - cbYOffset + cbLift + labelsShift, width: 12, height: 12 })
// place the paragraph below the checkboxes with extra spacing before and after the cost lines (~0.3cm)
try {
// increase gap around cost lines for better readability (~0.6cm)
const beforeCostGap = 17
const afterCostGap = 17
const paraStartY = baseY - gap * 6 + cbLift - 28 - beforeCostGap
const lineHeight = 14
// insert explicit gaps before and after the cost lines (lines 1-3)
let y = paraStartY
// first line
page.drawText(paraLines[0], { x: leftX, y: y, size: 11, font: helv })
y -= lineHeight
// gap before costs
y -= beforeCostGap
// cost lines (1..3)
for (let k = 1; k <= 3; k++) {
page.drawText(paraLines[k], { x: leftX + 6, y: y, size: 11, font: helv })
y -= lineHeight
}
// gap after costs
y -= afterCostGap
// remaining lines
for (let i = 4; i < paraLines.length; i++) {
page.drawText(paraLines[i], { x: leftX, y: y, size: 11, font: helv })
y -= lineHeight
}
// shift following content down by afterCostGap if needed (none after currently)
// now add signature/city line 2 cm below the current text
const twoCm = 56.7
const signY = y - twoCm
const sigText = 'Frankfurt/Main-Harheim, den'
const sigSize = 12
page.drawText(sigText + ' ', { x: leftX, y: signY, size: sigSize, font: helv })
const sigStartX = leftX + helv.widthOfTextAtSize(sigText + ' ', sigSize)
const sigWidth = 220
// draw thin line for signature
page.drawRectangle({ x: sigStartX, y: signY - 2, width: sigWidth, height: 1, color: rgb(0,0,0) })
// draw 'Datum' centered under the signature line
const datum = 'Datum'
const datumSize = 11
const datumX = sigStartX + sigWidth / 2 - helv.widthOfTextAtSize(datum, datumSize) / 2
page.drawText(datum, { x: datumX, y: signY - 18, size: datumSize, font: helv })
// create a date form field on page 1 centered under the date line
const dateFieldWidth = 120
const dateFieldX = sigStartX + sigWidth / 2 - dateFieldWidth / 2
// position date field so its bottom edge is exactly on the signature line
const signatureLineY = signY - 2 // the 1pt-high rectangle was drawn at signY-2
// raise date fields by 0.4cm (≈11.34pt) upward relative to the line
const dateRaise = 11.34
// For page 1 we need to move only the date input up by 5.2mm (≈14.739pt)
const signDatumExtraRaise = 14.739
form.createTextField('sign_datum').addToPage(page, { x: dateFieldX, y: signatureLineY - fieldHeight + signDatumExtraRaise, width: dateFieldWidth, height: fieldHeight, font: helv })
// second signature line 3cm below first
const threeCm = 85.04
const secondY = signY - threeCm
const line2Width = 300
page.drawRectangle({ x: leftX, y: secondY - 2, width: line2Width, height: 1, color: rgb(0,0,0) })
const label2 = 'Unterschrift (bei Jugendlichen gesetzlicher Vertreter)'
page.drawText(label2, { x: leftX, y: secondY - 18, size: 11, font: helv })
} catch (e) {
// ignore
}
// footer: right-aligned 'Seite 1 von 3' 2cm from bottom
const footerY = 56.7
const footerText = 'Seite 1 von 3'
const footerSize = 10
const footerWidth = helv.widthOfTextAtSize(footerText, footerSize)
page.drawText(footerText, { x: width - footerWidth - leftX, y: footerY, size: footerSize, font: helv })
// --- Add a second page with the same header and horizontal bar ---
const page2 = pdfDoc.addPage([595.28, 841.89])
const { width: width2, height: height2 } = page2.getSize()
const textWidth2 = helv.widthOfTextAtSize(headerText, headerSize)
const headerX2 = (width2 - textWidth2) / 2
const headerY2 = height2 - 72
page2.drawText(headerText, { x: headerX2, y: headerY2, size: headerSize, font: helv })
const barY2 = headerY2 - 20
page2.drawRectangle({ x: 0, y: barY2, width: width2, height: barHeight, color: rgb(0,0,0) })
// --- Page 2: SEPA-Lastschriftmandat text and fields ---
// move SEPA section slightly up so title sits closer to the header bar
const sepaYStart = headerY2 - 54
const sepaLeft = leftX
// increase line gap for SEPA section (~22pt)
const lineGap = 22
let sy = sepaYStart
const small = 11
page2.drawText('Erteilung eines SEPA-Lastschriftmandates', { x: sepaLeft, y: sy, size: 12, font: helvBold })
sy -= lineGap
// draw these two lines as a tight block (no extra gap)
page2.drawText('Harheimer Tischtennis-Club 1954 e.V.', { x: sepaLeft, y: sy, size: small, font: helv })
sy -= 12
page2.drawText('Unsere Gläubiger-Identifikationsnummer: DE46ZZZ00000745362', { x: sepaLeft, y: sy, size: small, font: helv })
// add 0.7cm vertical gap (≈19.8pt) before the authorization paragraph
sy -= 19.8
// draw the authorization text as wrapped lines within page margins
const wrapMaxWidth = width2 - sepaLeft - 48
function drawWrapped(text, x, y, size, font) {
const words = text.split(' ')
let line = ''
let curY = y
for (const w of words) {
const test = line ? line + ' ' + w : w
const testWidth = helv.widthOfTextAtSize(test, size)
if (testWidth > wrapMaxWidth) {
page2.drawText(line, { x, y: curY, size, font })
line = w
curY -= size + 4
} else {
line = test
}
}
if (line) {
page2.drawText(line, { x, y: curY, size, font })
curY -= size + 4
}
return curY
}
sy = drawWrapped('Hiermit ermächtige ich den Harheimer Tischtennis-Club 1954 e.V. die jährlichen Mitgliedsbeiträge von meinem untenstehenden Konto per Lastschrift einzuziehen. Zugleich weise ich mein Kreditinstitut an, die vom Harheimer Tischtennis-Club 1954 e.V. auf mein Konto gezogenen Lastschriften einzulösen.', sepaLeft, sy, small, helv)
sy -= lineGap * 0.2
// draw mandate reference and validity with a small gap
page2.drawText('Mandatsreferenz: HTC0000 _ _ _', { x: sepaLeft, y: sy, size: small, font: helv })
// add 0.7cm space after Mandatsreferenz as requested
sy -= 19.8
page2.drawText('Dieses Mandat gilt für die zugrundeliegende Beitrittserklärung ab sofort.', { x: sepaLeft, y: sy, size: small, font: helv })
sy -= lineGap * 0.6
// Draw labeled lines and create text fields for mandate details
const fieldHeight2 = 14
const fieldWidth2 = 380
const labelOffset = 0
// place SEPA inputs 1cm left (≈28.35pt) from previous and slightly up; adjust to align with labels
const inputX = sepaLeft + 220 - 28.35
// reduce vertical raise so inputs sit on same baseline as labels (previously too high)
const inputYAdjust = 6
// apply labelsShift only to the SEPA form labels/inputs so paragraphs remain unaffected
let syFields = sy + labelsShift
page2.drawText('Mitglied (Vorname und Name)', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv })
form.createTextField('sepa_mitglied').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv })
syFields -= lineGap * 1.1
page2.drawText('Kontoinhaber (Vorname und Name):', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv })
form.createTextField('sepa_kontoinhaber').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv })
syFields -= lineGap * 1.1
page2.drawText('Straße und Hausnummer:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv })
form.createTextField('sepa_strasse').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv })
syFields -= lineGap * 1.1
page2.drawText('PLZ und Ort:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv })
form.createTextField('sepa_plz_ort').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv })
syFields -= lineGap * 1.1
page2.drawText('Kreditinstitut:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv })
form.createTextField('sepa_bank').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv })
syFields -= lineGap * 1.1
page2.drawText('IBAN:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv })
form.createTextField('sepa_iban').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: Math.round(fieldWidth2 * fieldShrinkFactor), height: fieldHeight2, font: helv })
syFields -= lineGap * 1.1
page2.drawText('BIC:', { x: sepaLeft + labelOffset, y: syFields, size: small, font: helv })
// BIC remains full width as requested
form.createTextField('sepa_bic').addToPage(page2, { x: inputX, y: syFields - fieldHeight2 + 2 + inputYAdjust, width: 220, height: fieldHeight2, font: helv })
syFields -= lineGap * 1.1
// add signature and date lines 2cm below last field
const twoCm = 56.7
const signY2 = syFields - twoCm
// date text and line
const sigDateText = 'Frankfurt/Main-Harheim, den'
page2.drawText(sigDateText + ' ', { x: sepaLeft, y: signY2, size: 12, font: helv })
const sigDateStartX = sepaLeft + helv.widthOfTextAtSize(sigDateText + ' ', 12)
const sigDateWidth = 160
page2.drawRectangle({ x: sigDateStartX, y: signY2 - 2, width: sigDateWidth, height: 1, color: rgb(0,0,0) })
page2.drawText('Datum', { x: sigDateStartX + sigDateWidth / 2 - helv.widthOfTextAtSize('Datum', 11) / 2, y: signY2 - 18, size: 11, font: helv })
// create a date form field on page 2 centered under the date line
const sepaDateFieldWidth = 120
const sepaDateFieldX = sigDateStartX + sigDateWidth / 2 - sepaDateFieldWidth / 2
// position sepa date field so its bottom edge is on the signature line
// raise SEPA date field by the same amount so its top/bottom alignment matches requested position
form.createTextField('sepa_datum').addToPage(page2, { x: sepaDateFieldX, y: signY2 - 2 - fieldHeight2 + dateRaise, width: sepaDateFieldWidth, height: fieldHeight2, font: helv })
// footer on page 2: right-aligned 'Seite 2 von 3' 2cm from bottom
const footerText2 = 'Seite 2 von 3'
const footerSize2 = 10
const footerWidth2 = helv.widthOfTextAtSize(footerText2, footerSize2)
page2.drawText(footerText2, { x: width2 - footerWidth2 - leftX, y: 56.7, size: footerSize2, font: helv })
// signature line
// move signature label/line 2cm (≈56.7pt) further down
const signLineY = signY2 - 36 - 56.7
const signLineWidth = 300
page2.drawRectangle({ x: sepaLeft, y: signLineY - 2, width: signLineWidth, height: 1, color: rgb(0,0,0) })
page2.drawText('Unterschrift des Kontoinhabers', { x: sepaLeft, y: signLineY - 18, size: 11, font: helv })
// no form field for signature on page 2 (physical signature)
const pdfBytes = await pdfDoc.save()
fs.writeFileSync('server/templates/mitgliedschaft-fillable.pdf', pdfBytes)
console.log('Wrote server/templates/mitgliedschaft-fillable.pdf')
// --- Add a third page with same header/bar/footer and title 'Einwilligungserklärung' ---
const page3 = pdfDoc.addPage([595.28, 841.89])
const { width: width3, height: height3 } = page3.getSize()
const headerX3 = (width3 - textWidth) / 2
const headerY3 = height3 - 72
page3.drawText(headerText, { x: headerX3, y: headerY3, size: headerSize, font: helv })
const barY3 = headerY3 - 20
page3.drawRectangle({ x: 0, y: barY3, width: width3, height: barHeight, color: rgb(0,0,0) })
// title for page 3
const page3Title = 'Einwilligungserklärung'
const page3TitleSize = 14
const page3TitleX = leftX
const page3TitleY = headerY3 - 48
page3.drawText(page3Title, { x: page3TitleX, y: page3TitleY, size: page3TitleSize, font: helvBold })
// footer on page 3
const footerText3 = 'Seite 3 von 3'
const footerSize3 = 10
const footerWidth3 = helv.widthOfTextAtSize(footerText3, footerSize3)
page3.drawText(footerText3, { x: width3 - footerWidth3 - leftX, y: 56.7, size: footerSize3, font: helv })
}
create().catch(e => {
console.error(e)
process.exit(1)
})