Files
breakpilot-compliance/admin-compliance/lib/sdk/einwilligungen/export/pdf.ts
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

506 lines
13 KiB
TypeScript

// =============================================================================
// Privacy Policy PDF Export
// Export Datenschutzerklaerung to PDF format
// =============================================================================
import {
GeneratedPrivacyPolicy,
PrivacyPolicySection,
CompanyInfo,
SupportedLanguage,
DataPoint,
CATEGORY_METADATA,
RETENTION_PERIOD_INFO,
} from '../types'
// =============================================================================
// TYPES
// =============================================================================
export interface PDFExportOptions {
language: SupportedLanguage
includeTableOfContents: boolean
includeDataPointList: boolean
companyLogo?: string
primaryColor?: string
pageSize?: 'A4' | 'LETTER'
orientation?: 'portrait' | 'landscape'
fontSize?: number
}
const DEFAULT_OPTIONS: PDFExportOptions = {
language: 'de',
includeTableOfContents: true,
includeDataPointList: true,
primaryColor: '#6366f1',
pageSize: 'A4',
orientation: 'portrait',
fontSize: 11,
}
// =============================================================================
// PDF CONTENT STRUCTURE
// =============================================================================
export interface PDFSection {
type: 'title' | 'heading' | 'subheading' | 'paragraph' | 'table' | 'list' | 'pagebreak'
content?: string
items?: string[]
table?: {
headers: string[]
rows: string[][]
}
style?: {
color?: string
fontSize?: number
bold?: boolean
italic?: boolean
align?: 'left' | 'center' | 'right'
}
}
// =============================================================================
// PDF CONTENT GENERATION
// =============================================================================
/**
* Generate PDF content structure for Privacy Policy
*/
export function generatePDFContent(
policy: GeneratedPrivacyPolicy,
companyInfo: CompanyInfo,
dataPoints: DataPoint[],
options: Partial<PDFExportOptions> = {}
): PDFSection[] {
const opts = { ...DEFAULT_OPTIONS, ...options }
const sections: PDFSection[] = []
const lang = opts.language
// Title page
sections.push({
type: 'title',
content: lang === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy',
style: { color: opts.primaryColor, fontSize: 28, bold: true, align: 'center' },
})
sections.push({
type: 'paragraph',
content: lang === 'de'
? 'gemaess Art. 13, 14 DSGVO'
: 'according to Art. 13, 14 GDPR',
style: { fontSize: 14, align: 'center', italic: true },
})
// Company information
sections.push({
type: 'paragraph',
content: companyInfo.name,
style: { fontSize: 16, bold: true, align: 'center' },
})
sections.push({
type: 'paragraph',
content: `${companyInfo.address}, ${companyInfo.postalCode} ${companyInfo.city}`,
style: { align: 'center' },
})
sections.push({
type: 'paragraph',
content: `${lang === 'de' ? 'Stand' : 'Date'}: ${new Date(policy.generatedAt).toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US')}`,
style: { align: 'center' },
})
sections.push({
type: 'paragraph',
content: `Version: ${policy.version}`,
style: { align: 'center', fontSize: 10 },
})
sections.push({ type: 'pagebreak' })
// Table of Contents
if (opts.includeTableOfContents) {
sections.push({
type: 'heading',
content: lang === 'de' ? 'Inhaltsverzeichnis' : 'Table of Contents',
style: { color: opts.primaryColor },
})
const tocItems = policy.sections.map((section, idx) =>
`${idx + 1}. ${section.title[lang]}`
)
if (opts.includeDataPointList) {
tocItems.push(lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog')
}
sections.push({
type: 'list',
items: tocItems,
})
sections.push({ type: 'pagebreak' })
}
// Privacy Policy Sections
policy.sections.forEach((section, idx) => {
sections.push({
type: 'heading',
content: `${idx + 1}. ${section.title[lang]}`,
style: { color: opts.primaryColor },
})
// Convert markdown-like content to paragraphs
const content = section.content[lang]
const paragraphs = content.split('\n\n')
for (const para of paragraphs) {
if (para.startsWith('- ')) {
// List items
const items = para.split('\n').filter(l => l.startsWith('- ')).map(l => l.substring(2))
sections.push({
type: 'list',
items,
})
} else if (para.startsWith('### ')) {
sections.push({
type: 'subheading',
content: para.substring(4),
})
} else if (para.startsWith('## ')) {
sections.push({
type: 'subheading',
content: para.substring(3),
style: { bold: true },
})
} else if (para.trim()) {
sections.push({
type: 'paragraph',
content: para.replace(/\*\*(.*?)\*\*/g, '$1'), // Remove markdown bold for plain text
})
}
}
// Add related data points if this section has them
if (section.dataPointIds.length > 0 && opts.includeDataPointList) {
const relatedDPs = dataPoints.filter(dp => section.dataPointIds.includes(dp.id))
if (relatedDPs.length > 0) {
sections.push({
type: 'paragraph',
content: lang === 'de'
? `Betroffene Datenkategorien: ${relatedDPs.map(dp => dp.name[lang]).join(', ')}`
: `Affected data categories: ${relatedDPs.map(dp => dp.name[lang]).join(', ')}`,
style: { italic: true, fontSize: 10 },
})
}
}
})
// Data Point Catalog Appendix
if (opts.includeDataPointList && dataPoints.length > 0) {
sections.push({ type: 'pagebreak' })
sections.push({
type: 'heading',
content: lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog',
style: { color: opts.primaryColor },
})
sections.push({
type: 'paragraph',
content: lang === 'de'
? 'Die folgende Tabelle zeigt alle verarbeiteten personenbezogenen Daten:'
: 'The following table shows all processed personal data:',
})
// Group by category
const categories = [...new Set(dataPoints.map(dp => dp.category))]
for (const category of categories) {
const categoryDPs = dataPoints.filter(dp => dp.category === category)
const categoryMeta = CATEGORY_METADATA[category]
sections.push({
type: 'subheading',
content: `${categoryMeta.code}. ${categoryMeta.name[lang]}`,
})
sections.push({
type: 'table',
table: {
headers: lang === 'de'
? ['Code', 'Datenpunkt', 'Zweck', 'Loeschfrist']
: ['Code', 'Data Point', 'Purpose', 'Retention'],
rows: categoryDPs.map(dp => [
dp.code,
dp.name[lang],
dp.purpose[lang].substring(0, 50) + (dp.purpose[lang].length > 50 ? '...' : ''),
RETENTION_PERIOD_INFO[dp.retentionPeriod]?.label[lang] || dp.retentionPeriod,
]),
},
})
}
}
// Footer
sections.push({
type: 'paragraph',
content: lang === 'de'
? `Generiert am ${new Date().toLocaleDateString('de-DE')} mit dem Datenschutzerklaerung-Generator`
: `Generated on ${new Date().toLocaleDateString('en-US')} with the Privacy Policy Generator`,
style: { italic: true, align: 'center', fontSize: 9 },
})
return sections
}
// =============================================================================
// PDF BLOB GENERATION
// =============================================================================
/**
* Generate a PDF file as a Blob
* This generates HTML that can be printed to PDF or used with a PDF library
*/
export async function generatePDFBlob(
policy: GeneratedPrivacyPolicy,
companyInfo: CompanyInfo,
dataPoints: DataPoint[],
options: Partial<PDFExportOptions> = {}
): Promise<Blob> {
const content = generatePDFContent(policy, companyInfo, dataPoints, options)
const opts = { ...DEFAULT_OPTIONS, ...options }
// Generate HTML for PDF conversion
const html = generateHTMLFromContent(content, opts)
return new Blob([html], { type: 'text/html' })
}
/**
* Generate printable HTML from PDF content
*/
function generateHTMLFromContent(
content: PDFSection[],
options: PDFExportOptions
): string {
const pageWidth = options.pageSize === 'A4' ? '210mm' : '8.5in'
const pageHeight = options.pageSize === 'A4' ? '297mm' : '11in'
let html = `
<!DOCTYPE html>
<html lang="${options.language}">
<head>
<meta charset="utf-8">
<title>${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'}</title>
<style>
@page {
size: ${pageWidth} ${pageHeight};
margin: 20mm;
}
body {
font-family: 'Segoe UI', Calibri, Arial, sans-serif;
font-size: ${options.fontSize}pt;
line-height: 1.6;
color: #1e293b;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
font-size: 24pt;
color: ${options.primaryColor};
border-bottom: 2px solid ${options.primaryColor};
padding-bottom: 10px;
margin-top: 30px;
}
h2 {
font-size: 16pt;
color: ${options.primaryColor};
margin-top: 24px;
}
h3 {
font-size: 13pt;
color: #334155;
margin-top: 18px;
}
p {
margin: 12px 0;
text-align: justify;
}
.title {
font-size: 28pt;
text-align: center;
color: ${options.primaryColor};
font-weight: bold;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
font-style: italic;
color: #64748b;
}
.center {
text-align: center;
}
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 10pt;
}
th, td {
border: 1px solid #e2e8f0;
padding: 8px 12px;
text-align: left;
}
th {
background-color: ${options.primaryColor};
color: white;
font-weight: 600;
}
tr:nth-child(even) {
background-color: #f8fafc;
}
ul {
margin: 12px 0;
padding-left: 24px;
}
li {
margin: 6px 0;
}
.pagebreak {
page-break-after: always;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #e2e8f0;
font-size: 9pt;
color: #94a3b8;
text-align: center;
}
@media print {
body {
padding: 0;
}
.pagebreak {
page-break-after: always;
}
}
</style>
</head>
<body>
`
for (const section of content) {
switch (section.type) {
case 'title':
html += `<div class="title" style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</div>\n`
break
case 'heading':
html += `<h1 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h1>\n`
break
case 'subheading':
html += `<h3 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h3>\n`
break
case 'paragraph':
const alignClass = section.style?.align === 'center' ? ' class="center"' : ''
html += `<p${alignClass} style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</p>\n`
break
case 'list':
html += '<ul>\n'
for (const item of section.items || []) {
html += ` <li>${escapeHtml(item)}</li>\n`
}
html += '</ul>\n'
break
case 'table':
if (section.table) {
html += '<table>\n<thead><tr>\n'
for (const header of section.table.headers) {
html += ` <th>${escapeHtml(header)}</th>\n`
}
html += '</tr></thead>\n<tbody>\n'
for (const row of section.table.rows) {
html += '<tr>\n'
for (const cell of row) {
html += ` <td>${escapeHtml(cell)}</td>\n`
}
html += '</tr>\n'
}
html += '</tbody></table>\n'
}
break
case 'pagebreak':
html += '<div class="pagebreak"></div>\n'
break
}
}
html += '</body></html>'
return html
}
function getStyleString(style?: PDFSection['style']): string {
if (!style) return ''
const parts: string[] = []
if (style.color) parts.push(`color: ${style.color}`)
if (style.fontSize) parts.push(`font-size: ${style.fontSize}pt`)
if (style.bold) parts.push('font-weight: bold')
if (style.italic) parts.push('font-style: italic')
if (style.align) parts.push(`text-align: ${style.align}`)
return parts.join('; ')
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
// =============================================================================
// FILENAME GENERATION
// =============================================================================
/**
* Generate a filename for the PDF export
*/
export function generatePDFFilename(
companyInfo: CompanyInfo,
language: SupportedLanguage = 'de'
): string {
const companyName = companyInfo.name.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
const date = new Date().toISOString().split('T')[0]
const prefix = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy-Policy'
return `${prefix}-${companyName}-${date}.html`
}