// ============================================================================= // TOM Generator DOCX Export // Export TOMs to Microsoft Word format // ============================================================================= import { TOMGeneratorState, DerivedTOM, ControlCategory, CONTROL_CATEGORIES, } from '../types' import { getControlById, getCategoryMetadata } from '../controls/loader' // ============================================================================= // TYPES // ============================================================================= export interface DOCXExportOptions { language: 'de' | 'en' includeNotApplicable: boolean includeEvidence: boolean includeGapAnalysis: boolean companyLogo?: string primaryColor?: string } const DEFAULT_OPTIONS: DOCXExportOptions = { language: 'de', includeNotApplicable: false, includeEvidence: true, includeGapAnalysis: true, primaryColor: '#1a56db', } // ============================================================================= // DOCX CONTENT GENERATION // ============================================================================= export interface DocxParagraph { type: 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'bullet' 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 /** * Generate DOCX content structure for TOMs */ export function generateDOCXContent( state: TOMGeneratorState, options: Partial = {} ): DocxElement[] { const opts = { ...DEFAULT_OPTIONS, ...options } const elements: DocxElement[] = [] // Title page elements.push({ type: 'heading1', content: opts.language === 'de' ? 'Technische und Organisatorische Maßnahmen (TOMs)' : 'Technical and Organizational Measures (TOMs)', }) elements.push({ type: 'paragraph', content: opts.language === 'de' ? `gemäß Art. 32 DSGVO` : 'according to Art. 32 GDPR', }) // Company info if (state.companyProfile) { elements.push({ type: 'heading2', content: opts.language === 'de' ? 'Unternehmen' : 'Company', }) elements.push({ type: 'paragraph', content: `${state.companyProfile.name}`, }) elements.push({ type: 'paragraph', content: opts.language === 'de' ? `Branche: ${Array.isArray(state.companyProfile.industry) ? state.companyProfile.industry.join(', ') : state.companyProfile.industry}` : `Industry: ${Array.isArray(state.companyProfile.industry) ? state.companyProfile.industry.join(', ') : state.companyProfile.industry}`, }) elements.push({ type: 'paragraph', content: opts.language === 'de' ? `Rolle: ${formatRole(state.companyProfile.role, opts.language)}` : `Role: ${formatRole(state.companyProfile.role, opts.language)}`, }) if (state.companyProfile.dpoPerson) { elements.push({ type: 'paragraph', content: opts.language === 'de' ? `Datenschutzbeauftragter: ${state.companyProfile.dpoPerson}` : `Data Protection Officer: ${state.companyProfile.dpoPerson}`, }) } } // Document metadata elements.push({ type: 'paragraph', content: opts.language === 'de' ? `Stand: ${new Date().toLocaleDateString('de-DE')}` : `Date: ${new Date().toLocaleDateString('en-US')}`, }) // Protection level summary if (state.riskProfile) { elements.push({ type: 'heading2', content: opts.language === 'de' ? 'Schutzbedarf' : 'Protection Level', }) elements.push({ type: 'paragraph', content: opts.language === 'de' ? `Ermittelter Schutzbedarf: ${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}` : `Determined Protection Level: ${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}`, }) elements.push({ type: 'paragraph', content: opts.language === 'de' ? `CIA-Bewertung: Vertraulichkeit ${state.riskProfile.ciaAssessment.confidentiality}/5, Integrität ${state.riskProfile.ciaAssessment.integrity}/5, Verfügbarkeit ${state.riskProfile.ciaAssessment.availability}/5` : `CIA Assessment: Confidentiality ${state.riskProfile.ciaAssessment.confidentiality}/5, Integrity ${state.riskProfile.ciaAssessment.integrity}/5, Availability ${state.riskProfile.ciaAssessment.availability}/5`, }) if (state.riskProfile.dsfaRequired) { elements.push({ type: 'paragraph', content: opts.language === 'de' ? '⚠️ Eine Datenschutz-Folgenabschätzung (DSFA) ist erforderlich.' : '⚠️ A Data Protection Impact Assessment (DPIA) is required.', }) } } // TOMs by category elements.push({ type: 'heading2', content: opts.language === 'de' ? 'Übersicht der Maßnahmen' : 'Measures Overview', }) // Group TOMs by category const tomsByCategory = groupTOMsByCategory(state.derivedTOMs, opts.includeNotApplicable) for (const category of CONTROL_CATEGORIES) { const categoryTOMs = tomsByCategory.get(category.id) if (!categoryTOMs || categoryTOMs.length === 0) continue const categoryName = category.name[opts.language] elements.push({ type: 'heading3', content: `${categoryName} (${category.gdprReference})`, }) // Create table for this category const tableHeaders = opts.language === 'de' ? ['ID', 'Maßnahme', 'Typ', 'Status', 'Anwendbarkeit'] : ['ID', 'Measure', 'Type', 'Status', 'Applicability'] const tableRows: DocxTableRow[] = categoryTOMs.map((tom) => ({ cells: [ tom.controlId, tom.name, formatType(getControlById(tom.controlId)?.type || 'TECHNICAL', opts.language), formatImplementationStatus(tom.implementationStatus, opts.language), formatApplicability(tom.applicability, opts.language), ], })) elements.push({ type: 'table', headers: tableHeaders, rows: tableRows, }) // Add detailed descriptions for (const tom of categoryTOMs) { if (tom.applicability === 'NOT_APPLICABLE' && !opts.includeNotApplicable) { continue } elements.push({ type: 'paragraph', content: `**${tom.controlId}: ${tom.name}**`, }) elements.push({ type: 'paragraph', content: tom.aiGeneratedDescription || tom.description, }) elements.push({ type: 'bullet', content: opts.language === 'de' ? `Anwendbarkeit: ${formatApplicability(tom.applicability, opts.language)}` : `Applicability: ${formatApplicability(tom.applicability, opts.language)}`, }) elements.push({ type: 'bullet', content: opts.language === 'de' ? `Begründung: ${tom.applicabilityReason}` : `Reason: ${tom.applicabilityReason}`, }) elements.push({ type: 'bullet', content: opts.language === 'de' ? `Umsetzungsstatus: ${formatImplementationStatus(tom.implementationStatus, opts.language)}` : `Implementation Status: ${formatImplementationStatus(tom.implementationStatus, opts.language)}`, }) if (tom.responsiblePerson) { elements.push({ type: 'bullet', content: opts.language === 'de' ? `Verantwortlich: ${tom.responsiblePerson}` : `Responsible: ${tom.responsiblePerson}`, }) } if (opts.includeEvidence && tom.linkedEvidence.length > 0) { elements.push({ type: 'bullet', content: opts.language === 'de' ? `Nachweise: ${tom.linkedEvidence.length} Dokument(e) verknüpft` : `Evidence: ${tom.linkedEvidence.length} document(s) linked`, }) } if (tom.evidenceGaps.length > 0) { elements.push({ type: 'bullet', content: opts.language === 'de' ? `Fehlende Nachweise: ${tom.evidenceGaps.join(', ')}` : `Missing Evidence: ${tom.evidenceGaps.join(', ')}`, }) } } } // Gap Analysis if (opts.includeGapAnalysis && state.gapAnalysis) { elements.push({ type: 'heading2', content: opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis', }) elements.push({ type: 'paragraph', content: opts.language === 'de' ? `Gesamtscore: ${state.gapAnalysis.overallScore}%` : `Overall Score: ${state.gapAnalysis.overallScore}%`, }) if (state.gapAnalysis.missingControls.length > 0) { elements.push({ type: 'heading3', content: opts.language === 'de' ? 'Fehlende Maßnahmen' : 'Missing Measures', }) for (const missing of state.gapAnalysis.missingControls) { const control = getControlById(missing.controlId) elements.push({ type: 'bullet', content: `${missing.controlId}: ${control?.name[opts.language] || 'Unknown'} (${missing.priority})`, }) } } if (state.gapAnalysis.recommendations.length > 0) { elements.push({ type: 'heading3', content: opts.language === 'de' ? 'Empfehlungen' : 'Recommendations', }) for (const rec of state.gapAnalysis.recommendations) { elements.push({ type: 'bullet', content: rec, }) } } } // Footer elements.push({ type: 'paragraph', content: opts.language === 'de' ? `Dieses Dokument wurde automatisch generiert mit dem TOM Generator am ${new Date().toLocaleDateString('de-DE')}.` : `This document was automatically generated with the TOM Generator on ${new Date().toLocaleDateString('en-US')}.`, }) return elements } // ============================================================================= // HELPER FUNCTIONS // ============================================================================= function groupTOMsByCategory( toms: DerivedTOM[], includeNotApplicable: boolean ): Map { const grouped = new Map() for (const tom of toms) { if (!includeNotApplicable && tom.applicability === 'NOT_APPLICABLE') { continue } const control = getControlById(tom.controlId) if (!control) continue const category = control.category const existing = grouped.get(category) || [] existing.push(tom) grouped.set(category, existing) } return grouped } function formatRole(role: string, language: 'de' | 'en'): string { const roles: Record> = { CONTROLLER: { de: 'Verantwortlicher', en: 'Controller' }, PROCESSOR: { de: 'Auftragsverarbeiter', en: 'Processor' }, JOINT_CONTROLLER: { de: 'Gemeinsam Verantwortlicher', en: 'Joint Controller' }, } return roles[role]?.[language] || role } function formatProtectionLevel(level: string, language: 'de' | 'en'): string { const levels: Record> = { NORMAL: { de: 'Normal', en: 'Normal' }, HIGH: { de: 'Hoch', en: 'High' }, VERY_HIGH: { de: 'Sehr hoch', en: 'Very High' }, } return levels[level]?.[language] || level } function formatType(type: string, language: 'de' | 'en'): string { const types: Record> = { TECHNICAL: { de: 'Technisch', en: 'Technical' }, ORGANIZATIONAL: { de: 'Organisatorisch', en: 'Organizational' }, } return types[type]?.[language] || type } function formatImplementationStatus(status: string, language: 'de' | 'en'): string { const statuses: Record> = { NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' }, PARTIAL: { de: 'Teilweise umgesetzt', en: 'Partially Implemented' }, IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' }, } return statuses[status]?.[language] || status } function formatApplicability(applicability: string, language: 'de' | 'en'): string { const apps: Record> = { REQUIRED: { de: 'Erforderlich', en: 'Required' }, RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' }, OPTIONAL: { de: 'Optional', en: 'Optional' }, NOT_APPLICABLE: { de: 'Nicht anwendbar', en: 'Not Applicable' }, } return apps[applicability]?.[language] || applicability } // ============================================================================= // DOCX BLOB GENERATION // Uses simple XML structure compatible with docx libraries // ============================================================================= /** * Generate a DOCX file as a Blob * Note: For production, use docx library (npm install docx) * This is a simplified version that generates XML-based content */ export async function generateDOCXBlob( state: TOMGeneratorState, options: Partial = {} ): Promise { const content = generateDOCXContent(state, options) // Generate simple HTML that can be converted to DOCX // In production, use the docx library for proper DOCX generation const html = generateHTMLFromContent(content, options) // Return as a Word-compatible HTML blob // The proper way would be to use the docx library const blob = new Blob([html], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }) return blob } function generateHTMLFromContent( content: DocxElement[], options: Partial ): string { const opts = { ...DEFAULT_OPTIONS, ...options } let html = ` ` for (const element of content) { if (element.type === 'table') { html += '' html += '' for (const header of element.headers) { html += `` } html += '' for (const row of element.rows) { html += '' for (const cell of row.cells) { html += `` } html += '' } html += '
${escapeHtml(header)}
${escapeHtml(cell)}
' } else { const tag = getHtmlTag(element.type) const processedContent = processContent(element.content) html += `<${tag}>${processedContent}\n` } } html += '' return html } function getHtmlTag(type: string): string { switch (type) { 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, ''') } function processContent(content: string): string { // Convert markdown-style bold to HTML return escapeHtml(content).replace(/\*\*(.*?)\*\*/g, '$1') } // ============================================================================= // FILENAME GENERATION // ============================================================================= /** * Generate a filename for the DOCX export */ export function generateDOCXFilename( state: TOMGeneratorState, language: 'de' | 'en' = 'de' ): string { const companyName = state.companyProfile?.name?.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown' const date = new Date().toISOString().split('T')[0] const prefix = language === 'de' ? 'TOMs' : 'TOMs' return `${prefix}-${companyName}-${date}.docx` } // Types are exported at their definition site above