- tom-generator/export/zip.ts: extract private helpers to zip-helpers.ts (544→342 LOC) - tom-generator/export/docx.ts: extract private helpers to docx-helpers.ts (525→378 LOC) - tom-generator/export/pdf.ts: extract private helpers to pdf-helpers.ts (517→446 LOC) - tom-generator/demo-data/index.ts: extract DEMO_RISK_PROFILES + DEMO_EVIDENCE_DOCUMENTS to demo-data-part2.ts (518→360 LOC) - einwilligungen/generator/privacy-policy-sections.ts: extract sections 5-7 to part2 (559→313 LOC) - einwilligungen/export/pdf.ts: extract HTML/CSS helpers to pdf-helpers.ts (505→296 LOC) - vendor-compliance/context.tsx: extract API action hooks to context-actions.tsx (509→286 LOC) All originals re-export from sibling files — zero consumer import changes needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
297 lines
8.8 KiB
TypeScript
297 lines
8.8 KiB
TypeScript
// =============================================================================
|
|
// Privacy Policy PDF Export
|
|
// Export Datenschutzerklaerung to PDF format
|
|
// =============================================================================
|
|
|
|
import {
|
|
GeneratedPrivacyPolicy,
|
|
PrivacyPolicySection,
|
|
CompanyInfo,
|
|
SupportedLanguage,
|
|
DataPoint,
|
|
CATEGORY_METADATA,
|
|
RETENTION_PERIOD_INFO,
|
|
} from '../types'
|
|
import { generateHTMLFromContent } from './pdf-helpers'
|
|
|
|
// =============================================================================
|
|
// 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' })
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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`
|
|
}
|