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>
506 lines
13 KiB
TypeScript
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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`
|
|
}
|