// ============================================================================= // 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 } 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 = {} ): 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> = { 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 = {} ): Promise { 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 = ` ${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'} ` for (const element of content) { if (element.type === 'table') { html += '\n\n' for (const header of element.headers) { html += ` \n` } html += '\n\n' for (const row of element.rows) { html += '\n' for (const cell of row.cells) { html += ` \n` } html += '\n' } html += '
${escapeHtml(header)}
${escapeHtml(cell)}
\n' } else { const tag = getHtmlTag(element.type) const className = element.type === 'title' ? ' class="title"' : '' const processedContent = escapeHtml(element.content) html += `<${tag}${className}>${processedContent}\n` } } 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, ''') } // ============================================================================= // 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` }