Files
breakpilot-compliance/admin-compliance/lib/sdk/einwilligungen/export/docx.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

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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
// =============================================================================
// 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`
}