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>
494 lines
13 KiB
TypeScript
494 lines
13 KiB
TypeScript
// =============================================================================
|
|
// 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`
|
|
}
|