Implement PDF download functionality for membership applications; enhance application data handling in the API to support both encrypted and unencrypted formats. Update UI to display download button conditionally based on PDF generation status.

This commit is contained in:
Torsten Schulz (local)
2025-10-23 15:21:39 +02:00
parent d93312d03a
commit 9524a29b67
13 changed files with 207 additions and 26 deletions

View File

@@ -71,6 +71,16 @@
> >
Anzeigen Anzeigen
</button> </button>
<button
v-if="application.metadata.pdfGenerated"
@click="downloadPDF(application.id)"
class="px-3 py-1 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors flex items-center"
>
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
PDF
</button>
<button <button
v-if="application.status === 'pending'" v-if="application.status === 'pending'"
@click="approveApplication(application.id)" @click="approveApplication(application.id)"
@@ -182,6 +192,16 @@
> >
Schließen Schließen
</button> </button>
<button
v-if="selectedApplication.metadata.pdfGenerated"
@click="downloadPDF(selectedApplication.id)"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors flex items-center"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
PDF herunterladen
</button>
<button <button
v-if="selectedApplication.status === 'pending'" v-if="selectedApplication.status === 'pending'"
@click="approveApplication(selectedApplication.id)" @click="approveApplication(selectedApplication.id)"
@@ -268,6 +288,32 @@ const rejectApplication = async (id) => {
} }
} }
const downloadPDF = async (id) => {
try {
const filename = `beitrittserklärung_${id}.pdf`
// Direkter Download über die öffentliche Uploads-Route
const response = await fetch(`/uploads/${filename}`)
if (!response.ok) {
throw new Error('PDF nicht gefunden')
}
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (error) {
console.error('Fehler beim Herunterladen:', error)
alert('Fehler beim Herunterladen des PDFs')
}
}
const formatDate = (dateString) => { const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('de-DE', { return new Date(dateString).toLocaleDateString('de-DE', {
year: 'numeric', year: 'numeric',

View File

@@ -5,7 +5,7 @@ import { decryptObject } from '../../utils/encryption.js'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
try { try {
const config = useRuntimeConfig() const config = useRuntimeConfig()
const encryptionKey = config.encryptionKey const encryptionKey = config.encryptionKey || 'local_development_encryption_key_change_in_production'
if (!encryptionKey) { if (!encryptionKey) {
throw createError({ throw createError({
@@ -34,25 +34,35 @@ export default defineEventHandler(async (event) => {
const fileContent = await fs.readFile(filePath, 'utf8') const fileContent = await fs.readFile(filePath, 'utf8')
const applicationData = JSON.parse(fileContent) const applicationData = JSON.parse(fileContent)
// Verschlüsselte Daten entschlüsseln // Prüfe ob Daten verschlüsselt oder unverschlüsselt sind
const decryptedData = decryptObject(applicationData.encryptedData, encryptionKey) if (applicationData.personalData) {
// Unverschlüsselte Daten (neues Format)
applications.push({ applications.push({
id: applicationData.id, id: applicationData.id,
timestamp: applicationData.timestamp, timestamp: applicationData.timestamp,
status: applicationData.status, status: applicationData.status,
metadata: applicationData.metadata, metadata: applicationData.metadata,
// Entschlüsselte persönliche Daten personalData: applicationData.personalData
personalData: { })
nachname: decryptedData.nachname, } else if (applicationData.encryptedData) {
vorname: decryptedData.vorname, // Verschlüsselte Daten (altes Format)
email: decryptedData.email, const decryptedData = decryptObject(applicationData.encryptedData, encryptionKey)
telefon_privat: decryptedData.telefon_privat, applications.push({
telefon_mobil: decryptedData.telefon_mobil id: applicationData.id,
} timestamp: applicationData.timestamp,
}) status: applicationData.status,
metadata: applicationData.metadata,
personalData: {
nachname: decryptedData.nachname,
vorname: decryptedData.vorname,
email: decryptedData.email,
telefon_privat: decryptedData.telefon_privat,
telefon_mobil: decryptedData.telefon_mobil
}
})
}
} catch (error) { } catch (error) {
console.error(`Fehler beim Laden von ${file}:`, error) console.error(`Fehler beim Laden von ${file}:`, error.message)
} }
} }
} }

View File

@@ -228,10 +228,41 @@ export default defineEventHandler(async (event) => {
// Encrypt and save form data // Encrypt and save form data
try { try {
const password = process.env.ENCRYPTION_PASSWORD || 'default-password' const config = useRuntimeConfig()
const password = config.encryptionKey || 'local_development_encryption_key_change_in_production'
const encryptedData = encrypt(JSON.stringify(data), password) const encryptedData = encrypt(JSON.stringify(data), password)
const dataFilePath = path.join(uploadDir, result.filename.replace('.pdf', '.data')) const dataFilePath = path.join(uploadDir, result.filename.replace('.pdf', '.data'))
await fs.writeFile(dataFilePath, encryptedData) await fs.writeFile(dataFilePath, encryptedData)
// Also save application data for CMS
const applicationId = result.filename.replace('.pdf', '').replace('beitrittserklärung_', '')
const applicationData = {
id: applicationId,
timestamp: new Date().toISOString(),
status: 'pending',
metadata: {
mitgliedschaftsart: data.mitgliedschaftsart,
isVolljaehrig: data.isVolljaehrig,
pdfGenerated: true
},
// Temporär unverschlüsselte Daten für Testing
personalData: {
nachname: data.nachname,
vorname: data.vorname,
email: data.email,
telefon_privat: data.telefon_privat,
telefon_mobil: data.telefon_mobil
}
}
// Create membership applications directory
const applicationsDir = path.join(process.cwd(), 'server', 'data', 'membership-applications')
await fs.mkdir(applicationsDir, { recursive: true })
// Save application data
const applicationFilePath = path.join(applicationsDir, `${applicationId}.json`)
await fs.writeFile(applicationFilePath, JSON.stringify(applicationData, null, 2))
} catch (encryptError) { } catch (encryptError) {
console.error('Fehler beim Verschlüsseln der Daten:', encryptError.message) console.error('Fehler beim Verschlüsseln der Daten:', encryptError.message)
// Continue without encryption for now // Continue without encryption for now

View File

@@ -16,5 +16,16 @@
"phone": "069 234567", "phone": "069 234567",
"address": "Hauptstraße 5, 60437 Frankfurt", "address": "Hauptstraße 5, 60437 Frankfurt",
"notes": "Damen" "notes": "Damen"
},
{
"firstName": "Test",
"lastName": "Antrag",
"email": "test@antrag.de",
"phone": "",
"address": "Teststr 1, 60437 Frankfurt",
"notes": "Mitgliedschaftsart: aktiv | Genehmigt: 23.10.2025",
"source": "membership_application",
"applicationId": "1761225361334",
"id": "c209cb36-3aca-4ab2-b463-061e41f97d6d"
} }
] ]

View File

@@ -0,0 +1,11 @@
{
"id": "1761225329627",
"timestamp": "2025-10-23T13:15:29.679Z",
"status": "pending",
"metadata": {
"mitgliedschaftsart": "aktiv",
"isVolljaehrig": true,
"pdfGenerated": true
},
"encryptedData": "aQzeFqkSQwv/FSQ3ljTOJum1DXQrepjipc4AO0B/c7i2eSNVG4mWVN3HLq1dTzfE/TbN/pOw36sTZ04Jlk/LO2o4Zxtyr/EqXGGa313rjCoLavRUaDNLQLHPt7OBvQ69W7IOJBhIQ7SMMMmCr8isrETV7Dx9i7WyylpOWZpACdY1DpF7KBcfG4RJDRzCd+Nv3GsTJZbP07+24d1+suobT5pMMtoRFX4l4dgU2HPBuovDFFdSeLmD3BLx5FkZmzoTU7OQRB7tIKDGj0nAAuYueGsCFEc9oa2vhmIwr8dtoIC5fNy3burxEBjcXF+LTAulJxNLS5JFBqD+Xz5rjRUJPj+BeR0CKW9YMa6R+IaNN8c92oj/WeQgilGCWx8vSe37mFq00tSC7jpn5KrUQXZ0pu6KDwdOoKBzkJTNmBkMVufe4TxCiUDXnJCKZFdquYmqtqPwwjJX3sej2OE0CltktAH5g4od8JVLnYdqek0nHAJWuVm3aR9Gtq10JK44qkyThRQMHtjBc5aM+IAtVL56j8BKS5kLY6HvH30mfAaIJSZv1Ume3CnsOs3vaRioftPkkIcOyhVpyRsM5w0gcZQtn35MB/sZxXeve31AT8BKy0gi80IpDvcBhLPnPbeNgaSuXlVoyCof8Hls6j/W4i0v/OHFqj8JRTbx3JCJ0ijGS00="
}

View File

@@ -0,0 +1,11 @@
{
"id": "1761225352124",
"timestamp": "2025-10-23T13:15:52.174Z",
"status": "pending",
"metadata": {
"mitgliedschaftsart": "aktiv",
"isVolljaehrig": true,
"pdfGenerated": true
},
"encryptedData": "lz9bGXmeaZTFW8002nJ4lh6OCfCn/2bojD3n6Ul/lkxpdiuxhQ8pVBRO8TZ92R7+xRBNsD0Du6tHTFQJMG6VJN9Du18fTqvXSKcpeIXS/2mSqWGpP9Pb4Lg+bgdlIl1cdY+2th+KNM9bmyJSnsyQyzYi/yHrR6gJqKZUO5oxS71fTQYb9mpaR4XUvPn+pNEguShymM0bo5njdPpx4BLiyJTtij0nZtzNSTDPM4sJ6s/G8XxTXg1mXT7Z2QCOpRSAg2z8IwkvTB88+SDFgy4ssyBQGzHnCZS6A09bZoa7SuK8hvekPzPxPzfOjiZ5Ze4Ay8hALaWep6qiAfKrcDUAoYKntve9P1h9ROv+RUUNe/5padl3NarD+fVg8F5ol6qM53OaTSiRh0SuEPxt97R8k9FYKNYQZt6BIFuuvTexNnYAVDgjtux5ut+JlO1lpIxRZ6nzqgr8do4KWnigl6PyIYYcCcP6BU3BzuSSFmjc85phLh38iuCu+f5CjnKBUUqohrZZzMCVEg3S+NrqT3Mnzj+urnRYWB4XN0xsbUnZ3JarFyCFzmLUuqsZ2IdyNxAYJwNlo1tKpxEi/WDn/cHfd6hciXXVtfDrUK/Z93pQvEDAWaWwCnvCuoE8Cb+HnaQz1440VGzrpHCbMNLz2+s3XYbtUMO2iInOHrKw9hMB3w/L5eM3fXhuy48A48Xm8CvT"
}

View File

@@ -0,0 +1,12 @@
{
"id": "1761225361334",
"timestamp": "2025-10-23T13:16:01.377Z",
"status": "approved",
"metadata": {
"mitgliedschaftsart": "aktiv",
"isVolljaehrig": true,
"pdfGenerated": true
},
"encryptedData": "U0NuZGiyTzvR2jHezWmCrGqJN8zy5/ylPVeHoIDrPytXvHp2z7L5QFswTiJSevTPIqAmtILtlsmuZstfavplHqoSzd4Vgd6p3vnvXdwlk6jJkt6Aups56XU/MvQ0G8Xh3gqscB6rvaCP3xmtUtTtLK4SJz80PmZKmjABWQRYQhPRoZTNhuOgZpHgbW8jFKdrjqdes+gJav+CCkrsaGuMqs+/X6X5yUW/it0e7ZzMhEwb0XeDUhGB1jPlZWzgDlzaThVE4WDbG6Q3t7d4ZJzG3FkXkUq9kA3qFVbVmXLcxBVKURIzjuzLeJmoYxYPLNLY0J5Ou6RPMX1xeIPbaDPHRTRJsTM+qsebx3lAddmj4/LFb8gTA4T45kUb+3TfjesbIuNXW/0lf0vuOEI+wkQSWb863ar2K68Zt38XAvq7eSSWp56PfkCx/4URPhrgJBttqWChO1SeoqmR+zP1s4HVOjSXOvxcHIr7YejmAo6MVmFFobX8MjcR8og1texh8RmMUCuUY4cJgEGaUWubrJjV+hEFPnDaPDu6+usS0MHxsMcQuhZjGJejsNiaG9sLERIdNw49jt/0GfL7nICsPtQ78YdWr5tGUy5wCA19L6KrXu0ERs6X5w4hX0OWd4DbClQFI+KJE+QLQwRfD4k3jaSkqfY+ofjIlbPSou/z8m94OJ0=",
"updatedAt": "2025-10-23T13:18:08.669Z"
}

View File

@@ -0,0 +1,12 @@
{
"id": "1761225418055",
"timestamp": "2025-10-23T13:16:58.106Z",
"status": "rejected",
"metadata": {
"mitgliedschaftsart": "aktiv",
"isVolljaehrig": true,
"pdfGenerated": true
},
"encryptedData": "3FsakUVgkd7SlVyJCA9u88XDXLrRfPkgGiIfWr1gE492TTXT7M81XTpFnUcpSzJoesMbP8bbtWooicd3ae5rfEI9yRRVYdIss/I4cUnMILxqhEmaS2a+AFJOq3nDd3yJu8/n8OfcRQKbnl5OfujdzqyWw/PZ9Jb3IzvSLuVeaYe3UF0XFGrDi69EAErQbcRKv5EImoOWk4bGcGlx5dKWKMYVzSF/TXxHbMmbxoUrQ01Ru7Gyk4+ELFuXa6ItNd5W9uAapz8hHrWCoIx/umO1tDP7Ont/p8WvchkyGjT6oiKlhX+MhkCHUwGwTy6Dg5GWNvZYVMklLMosFmWYoOc4d6x1CgBq0gX3xCnP+n0MMV0cZ2+fgYUy0VIQSF5+qWNWTYAtJsUaVEIiYb8qkdPseLvKYaQ7owGmXZyaC5R/H26Ydqd3BaLEGEm0bj1fcOOR3g0umrR/ReogS0pZOExMuWiri4+cQooWRfyicrUJSnkvSFlozqWPA+5brCmGvmaZ01yvbwqYPRBtd6URt+zlxf9h4Q4dZ7WiTTyQXY+6KjhW+junmZZWt5LRvYx+kSk6UO2b/UuXnVQokieCo+a6dZG6lMtW/G7rPTOZ4bFMWLJR2v8U8OeeoY943zPU9svQv/3+C8qWxXkrTpJR4xINetRpiuC7jg/l+BoGdH6LZEyFuUPLkFoU2B81rSwz+mC2TDbGcnPVeF7la01auZfbZ26omaHqt0Qd9TE4pbl1FyBlADtmbyJGE1YlsHS20i4nnYQSXn61gw6tCos5aiv/5ZGwb1ek1TxLD8P5i5oSjPk=",
"updatedAt": "2025-10-23T13:17:24.340Z"
}

View File

@@ -0,0 +1,11 @@
{
"id": "1761225601268",
"timestamp": "2025-10-23T13:20:01.318Z",
"status": "pending",
"metadata": {
"mitgliedschaftsart": "aktiv",
"isVolljaehrig": true,
"pdfGenerated": true
},
"encryptedData": "36LAoZkl2k+In6oBsjTa9ttCGdeCkdOL/AGTIw7O8/v5BRaB7t0WLnuua7kr9Juv81YyczVFQbyK2n590KGigAWLyTeBGUimmuv+T/lQ35rhmeT7KcFzskZ+4mLz0XaeO5VMtDMmOlYWe5HWflM8Z8bqRtMHutzMUv0mOQFcvLIMrlIAA8QJgKpoaA9QDUKycMWE+a7ygPQtmbwegRYTOmjj56roLX74B6m4TOvv1RfVJXepBIJMqkkCEyttogbl5MK1fB8Z5TcZZkUNzR5C2DvJEN6ru0qIs3hl/CZshGc4/ukD0ufC+OHSGFX3KH+WXxy4F4tGT0nA6xgNanZDuMpmXecVjExu/viEP9z/uItD1ZJ9eaIT150Wh36PF2gzAz4vZETEAzuLpkiHQn8NQU41yp2Teudvl18CvXv5fVqTmTlex0tVdqVqTJi2UxxEyoay7Qt8Apjdve4rppomqAGIYxL5Tbhy+eKZ9nD4cxLlaSGFgD3+8IDQFuatFwCOUpjMsnUFRGyAiJvVYXGi58ail+yKqXVj2YNlEIopKC0U2XtehNfgVVC1+pk/wZwALOPYC+Io9ZMhogLIPk4q23rBKW0Gs9JjMPP4+Een8AavIFcrQ2mb4koRW+FDd3EmyrwHUS6/9Uj0vPFi/w3LeCm2ZITSDj3M6je9Xkr09jk="
}

View File

@@ -0,0 +1,11 @@
{
"id": "1761225630365",
"timestamp": "2025-10-23T13:20:30.435Z",
"status": "pending",
"metadata": {
"mitgliedschaftsart": "aktiv",
"isVolljaehrig": true,
"pdfGenerated": true
},
"encryptedData": "7/HlHstjzGZ1wJtrtCxa7B3gfqBxm19LkzNAItSM9rMGDvRoiQNrHIpxdlUSTOTkeYwfW3cvKhrBsXHZxU4P64HdFzVdK255kgN1Yqy2ZqS6oZ6mp8xwHVIUypfbwEZI22gWbxue9Y+29rn9VYSBuKjtZd2pi/8Qjohps4wchzXdP5fiRJxx3vwDHPQ08i+qAcxatIwYLt4Iu46pFSAEQkUhDbdki7OWgzeU4YW8L9UufPMMv01m2rG48GnnM/GglIkz2VchqQsNqsDmm35OUmg6LoETGs6TSgBiLr0LV+ZFO29D9W7WIP+hZDVmhfii8PNcAGvvKpJvpJSbNeswaSF4zV7k3+gxkcGZBy+hG+ta3pXhzzuaGqQUvyg+jx5YkMTVRh0kcQlmNME8/7UW5vrC+Qe/pU5K2t0URwxxHTYnLyTk0FLMrs6nCtaSSoDC+t26j8GarAGGF9AvKDaVxkFq4B9B0uw6mQPhETX8IeAynihKkk/ZMzIXB0LaO+hxW2YEKrKfFMSAVcFrVOjVJtz4k+5ZTRlKnjkunl2P45h+Fjt69OQNffg2F0Fa1Or9nrPOrbake7g+NEtAgohdQd7qddvpSBfJC3vDQv5TG2CONFIoSBHnpR3io7uuiI6Ct/mu/LBLfDa9d0/TjkOWdDVYnb+V+beh/At+oMUGKI8="
}

View File

@@ -0,0 +1,15 @@
{
"id": "1761225654593",
"timestamp": "2025-10-23T13:20:54.638Z",
"status": "pending",
"metadata": {
"mitgliedschaftsart": "aktiv",
"isVolljaehrig": true,
"pdfGenerated": true
},
"personalData": {
"nachname": "Test",
"vorname": "Working",
"email": "working@test.de"
}
}

View File

@@ -105,10 +105,10 @@
"expiresAt": "2025-10-29T12:38:53.918Z" "expiresAt": "2025-10-29T12:38:53.918Z"
}, },
{ {
"id": "1761144008551", "id": "1761225427206",
"userId": "1", "userId": "1",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJlbWFpbCI6ImFkbWluQGhhcmhlaW1lcnRjLmRlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzYxMTQ0MDA4LCJleHAiOjE3NjE3NDg4MDh9.G5OtippK77mTioWV8Js-OpoJmaEPLA38VRjwRRj93oA", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJlbWFpbCI6ImFkbWluQGhhcmhlaW1lcnRjLmRlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzYxMjI1NDI3LCJleHAiOjE3NjE4MzAyMjd9.MANlBkbOU95y-6yd51m0-hoa941A0uHutVwzl481k9I",
"createdAt": "2025-10-22T14:40:08.551Z", "createdAt": "2025-10-23T13:17:07.206Z",
"expiresAt": "2025-10-29T14:40:08.551Z" "expiresAt": "2025-10-30T13:17:07.206Z"
} }
] ]

View File

@@ -8,6 +8,6 @@
"phone": "", "phone": "",
"active": true, "active": true,
"created": "2025-10-21T00:00:00.000Z", "created": "2025-10-21T00:00:00.000Z",
"lastLogin": "2025-10-22T14:40:08.552Z" "lastLogin": "2025-10-23T13:17:07.206Z"
} }
] ]