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>
This commit is contained in:
493
admin-compliance/lib/sdk/einwilligungen/export/docx.ts
Normal file
493
admin-compliance/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`
|
||||
}
|
||||
Reference in New Issue
Block a user