fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
493
admin-v2/lib/sdk/einwilligungen/export/docx.ts
Normal file
493
admin-v2/lib/sdk/einwilligungen/export/docx.ts
Normal file
@@ -0,0 +1,493 @@
|
||||
// =============================================================================
|
||||
// Privacy Policy DOCX Export
|
||||
// Export Datenschutzerklaerung to Microsoft Word format
|
||||
// =============================================================================
|
||||
|
||||
import {
|
||||
GeneratedPrivacyPolicy,
|
||||
PrivacyPolicySection,
|
||||
CompanyInfo,
|
||||
SupportedLanguage,
|
||||
DataPoint,
|
||||
CATEGORY_METADATA,
|
||||
RETENTION_PERIOD_INFO,
|
||||
} from '../types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface DOCXExportOptions {
|
||||
language: SupportedLanguage
|
||||
includeTableOfContents: boolean
|
||||
includeDataPointList: boolean
|
||||
companyLogo?: string
|
||||
primaryColor?: string
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: DOCXExportOptions = {
|
||||
language: 'de',
|
||||
includeTableOfContents: true,
|
||||
includeDataPointList: true,
|
||||
primaryColor: '#6366f1',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DOCX CONTENT STRUCTURE
|
||||
// =============================================================================
|
||||
|
||||
export interface DocxParagraph {
|
||||
type: 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'bullet' | 'title'
|
||||
content: string
|
||||
style?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface DocxTableRow {
|
||||
cells: string[]
|
||||
isHeader?: boolean
|
||||
}
|
||||
|
||||
export interface DocxTable {
|
||||
type: 'table'
|
||||
headers: string[]
|
||||
rows: DocxTableRow[]
|
||||
}
|
||||
|
||||
export type DocxElement = DocxParagraph | DocxTable
|
||||
|
||||
// =============================================================================
|
||||
// DOCX CONTENT GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate DOCX content structure for Privacy Policy
|
||||
*/
|
||||
export function generateDOCXContent(
|
||||
policy: GeneratedPrivacyPolicy,
|
||||
companyInfo: CompanyInfo,
|
||||
dataPoints: DataPoint[],
|
||||
options: Partial<DOCXExportOptions> = {}
|
||||
): DocxElement[] {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
const elements: DocxElement[] = []
|
||||
const lang = opts.language
|
||||
|
||||
// Title
|
||||
elements.push({
|
||||
type: 'title',
|
||||
content: lang === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy',
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: lang === 'de'
|
||||
? 'gemaess Art. 13, 14 DSGVO'
|
||||
: 'according to Art. 13, 14 GDPR',
|
||||
style: { fontStyle: 'italic', textAlign: 'center' },
|
||||
})
|
||||
|
||||
// Company Info
|
||||
elements.push({
|
||||
type: 'heading2',
|
||||
content: lang === 'de' ? 'Verantwortlicher' : 'Controller',
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: companyInfo.name,
|
||||
style: { fontWeight: 'bold' },
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${companyInfo.address}`,
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${companyInfo.postalCode} ${companyInfo.city}`,
|
||||
})
|
||||
|
||||
if (companyInfo.country) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: companyInfo.country,
|
||||
})
|
||||
}
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${lang === 'de' ? 'E-Mail' : 'Email'}: ${companyInfo.email}`,
|
||||
})
|
||||
|
||||
if (companyInfo.phone) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${lang === 'de' ? 'Telefon' : 'Phone'}: ${companyInfo.phone}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (companyInfo.website) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `Website: ${companyInfo.website}`,
|
||||
})
|
||||
}
|
||||
|
||||
// DPO Info
|
||||
if (companyInfo.dpoName || companyInfo.dpoEmail) {
|
||||
elements.push({
|
||||
type: 'heading3',
|
||||
content: lang === 'de' ? 'Datenschutzbeauftragter' : 'Data Protection Officer',
|
||||
})
|
||||
|
||||
if (companyInfo.dpoName) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: companyInfo.dpoName,
|
||||
})
|
||||
}
|
||||
|
||||
if (companyInfo.dpoEmail) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${lang === 'de' ? 'E-Mail' : 'Email'}: ${companyInfo.dpoEmail}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (companyInfo.dpoPhone) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${lang === 'de' ? 'Telefon' : 'Phone'}: ${companyInfo.dpoPhone}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Document metadata
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: lang === 'de'
|
||||
? `Stand: ${new Date(policy.generatedAt).toLocaleDateString('de-DE')}`
|
||||
: `Date: ${new Date(policy.generatedAt).toLocaleDateString('en-US')}`,
|
||||
style: { marginTop: '20px' },
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `Version: ${policy.version}`,
|
||||
})
|
||||
|
||||
// Table of Contents
|
||||
if (opts.includeTableOfContents) {
|
||||
elements.push({
|
||||
type: 'heading2',
|
||||
content: lang === 'de' ? 'Inhaltsverzeichnis' : 'Table of Contents',
|
||||
})
|
||||
|
||||
policy.sections.forEach((section, idx) => {
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: `${idx + 1}. ${section.title[lang]}`,
|
||||
})
|
||||
})
|
||||
|
||||
if (opts.includeDataPointList) {
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Privacy Policy Sections
|
||||
policy.sections.forEach((section, idx) => {
|
||||
elements.push({
|
||||
type: 'heading1',
|
||||
content: `${idx + 1}. ${section.title[lang]}`,
|
||||
})
|
||||
|
||||
// Parse content
|
||||
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('- '))
|
||||
for (const item of items) {
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: item.substring(2),
|
||||
})
|
||||
}
|
||||
} else if (para.startsWith('### ')) {
|
||||
elements.push({
|
||||
type: 'heading3',
|
||||
content: para.substring(4),
|
||||
})
|
||||
} else if (para.startsWith('## ')) {
|
||||
elements.push({
|
||||
type: 'heading2',
|
||||
content: para.substring(3),
|
||||
})
|
||||
} else if (para.trim()) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: para.replace(/\*\*(.*?)\*\*/g, '$1'),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Data Point Catalog Appendix
|
||||
if (opts.includeDataPointList && dataPoints.length > 0) {
|
||||
elements.push({
|
||||
type: 'heading1',
|
||||
content: lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog',
|
||||
})
|
||||
|
||||
elements.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]
|
||||
|
||||
elements.push({
|
||||
type: 'heading3',
|
||||
content: `${categoryMeta.code}. ${categoryMeta.name[lang]}`,
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'table',
|
||||
headers: lang === 'de'
|
||||
? ['Code', 'Datenpunkt', 'Rechtsgrundlage', 'Loeschfrist']
|
||||
: ['Code', 'Data Point', 'Legal Basis', 'Retention'],
|
||||
rows: categoryDPs.map(dp => ({
|
||||
cells: [
|
||||
dp.code,
|
||||
dp.name[lang],
|
||||
formatLegalBasis(dp.legalBasis, lang),
|
||||
RETENTION_PERIOD_INFO[dp.retentionPeriod]?.label[lang] || dp.retentionPeriod,
|
||||
],
|
||||
})),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: lang === 'de'
|
||||
? `Dieses Dokument wurde automatisch generiert mit dem Datenschutzerklaerung-Generator am ${new Date().toLocaleDateString('de-DE')}.`
|
||||
: `This document was automatically generated with the Privacy Policy Generator on ${new Date().toLocaleDateString('en-US')}.`,
|
||||
style: { fontStyle: 'italic', fontSize: '9pt' },
|
||||
})
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function formatLegalBasis(basis: string, language: SupportedLanguage): string {
|
||||
const bases: Record<string, Record<SupportedLanguage, string>> = {
|
||||
CONTRACT: { de: 'Vertrag (Art. 6 Abs. 1 lit. b)', en: 'Contract (Art. 6(1)(b))' },
|
||||
CONSENT: { de: 'Einwilligung (Art. 6 Abs. 1 lit. a)', en: 'Consent (Art. 6(1)(a))' },
|
||||
LEGITIMATE_INTEREST: { de: 'Ber. Interesse (Art. 6 Abs. 1 lit. f)', en: 'Legitimate Interest (Art. 6(1)(f))' },
|
||||
LEGAL_OBLIGATION: { de: 'Rechtspflicht (Art. 6 Abs. 1 lit. c)', en: 'Legal Obligation (Art. 6(1)(c))' },
|
||||
}
|
||||
return bases[basis]?.[language] || basis
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DOCX BLOB GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a DOCX file as a Blob
|
||||
* This generates HTML that Word can open
|
||||
*/
|
||||
export async function generateDOCXBlob(
|
||||
policy: GeneratedPrivacyPolicy,
|
||||
companyInfo: CompanyInfo,
|
||||
dataPoints: DataPoint[],
|
||||
options: Partial<DOCXExportOptions> = {}
|
||||
): Promise<Blob> {
|
||||
const content = generateDOCXContent(policy, companyInfo, dataPoints, options)
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
|
||||
const html = generateHTMLFromContent(content, opts)
|
||||
|
||||
return new Blob([html], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
})
|
||||
}
|
||||
|
||||
function generateHTMLFromContent(
|
||||
content: DocxElement[],
|
||||
options: DOCXExportOptions
|
||||
): string {
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="${options.language}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Calibri, Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.5;
|
||||
color: #1e293b;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18pt;
|
||||
color: ${options.primaryColor};
|
||||
border-bottom: 1px solid ${options.primaryColor};
|
||||
padding-bottom: 8px;
|
||||
margin-top: 24pt;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14pt;
|
||||
color: ${options.primaryColor};
|
||||
margin-top: 18pt;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 12pt;
|
||||
color: #334155;
|
||||
margin-top: 14pt;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24pt;
|
||||
color: ${options.primaryColor};
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12pt;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8pt 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12pt 0;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #cbd5e1;
|
||||
padding: 6pt 10pt;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: ${options.primaryColor};
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 8pt 0;
|
||||
padding-left: 20pt;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4pt 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
`
|
||||
|
||||
for (const element of content) {
|
||||
if (element.type === 'table') {
|
||||
html += '<table>\n<thead><tr>\n'
|
||||
for (const header of element.headers) {
|
||||
html += ` <th>${escapeHtml(header)}</th>\n`
|
||||
}
|
||||
html += '</tr></thead>\n<tbody>\n'
|
||||
for (const row of element.rows) {
|
||||
html += '<tr>\n'
|
||||
for (const cell of row.cells) {
|
||||
html += ` <td>${escapeHtml(cell)}</td>\n`
|
||||
}
|
||||
html += '</tr>\n'
|
||||
}
|
||||
html += '</tbody></table>\n'
|
||||
} else {
|
||||
const tag = getHtmlTag(element.type)
|
||||
const className = element.type === 'title' ? ' class="title"' : ''
|
||||
const processedContent = escapeHtml(element.content)
|
||||
html += `<${tag}${className}>${processedContent}</${tag}>\n`
|
||||
}
|
||||
}
|
||||
|
||||
html += '</body></html>'
|
||||
return html
|
||||
}
|
||||
|
||||
function getHtmlTag(type: string): string {
|
||||
switch (type) {
|
||||
case 'title':
|
||||
return 'div'
|
||||
case 'heading1':
|
||||
return 'h1'
|
||||
case 'heading2':
|
||||
return 'h2'
|
||||
case 'heading3':
|
||||
return 'h3'
|
||||
case 'bullet':
|
||||
return 'li'
|
||||
default:
|
||||
return 'p'
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FILENAME GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a filename for the DOCX export
|
||||
*/
|
||||
export function generateDOCXFilename(
|
||||
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}.doc`
|
||||
}
|
||||
8
admin-v2/lib/sdk/einwilligungen/export/index.ts
Normal file
8
admin-v2/lib/sdk/einwilligungen/export/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Einwilligungen Export Module
|
||||
*
|
||||
* PDF and DOCX export functionality for Privacy Policy documents.
|
||||
*/
|
||||
|
||||
export * from './pdf'
|
||||
export * from './docx'
|
||||
505
admin-v2/lib/sdk/einwilligungen/export/pdf.ts
Normal file
505
admin-v2/lib/sdk/einwilligungen/export/pdf.ts
Normal file
@@ -0,0 +1,505 @@
|
||||
// =============================================================================
|
||||
// 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`
|
||||
}
|
||||
Reference in New Issue
Block a user