Files
breakpilot-compliance/admin-compliance/lib/sdk/tom-generator/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

526 lines
16 KiB
TypeScript

// =============================================================================
// 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<string, string>
}
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<DOCXExportOptions> = {}
): 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: ${state.companyProfile.industry}`
: `Industry: ${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<ControlCategory, DerivedTOM[]> {
const grouped = new Map<ControlCategory, DerivedTOM[]>()
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<string, Record<'de' | 'en', string>> = {
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<string, Record<'de' | 'en', string>> = {
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<string, Record<'de' | 'en', string>> = {
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<string, Record<'de' | 'en', string>> = {
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<string, Record<'de' | 'en', string>> = {
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<DOCXExportOptions> = {}
): Promise<Blob> {
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<DOCXExportOptions>
): string {
const opts = { ...DEFAULT_OPTIONS, ...options }
let html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Calibri, Arial, sans-serif; font-size: 11pt; line-height: 1.5; }
h1 { font-size: 24pt; color: ${opts.primaryColor}; border-bottom: 2px solid ${opts.primaryColor}; }
h2 { font-size: 18pt; color: ${opts.primaryColor}; margin-top: 24pt; }
h3 { font-size: 14pt; color: #333; margin-top: 18pt; }
table { border-collapse: collapse; width: 100%; margin: 12pt 0; }
th, td { border: 1px solid #ddd; padding: 8pt; text-align: left; }
th { background-color: ${opts.primaryColor}; color: white; }
tr:nth-child(even) { background-color: #f9f9f9; }
ul { margin: 6pt 0; }
li { margin: 3pt 0; }
.warning { color: #dc2626; font-weight: bold; }
</style>
</head>
<body>
`
for (const element of content) {
if (element.type === 'table') {
html += '<table>'
html += '<tr>'
for (const header of element.headers) {
html += `<th>${escapeHtml(header)}</th>`
}
html += '</tr>'
for (const row of element.rows) {
html += '<tr>'
for (const cell of row.cells) {
html += `<td>${escapeHtml(cell)}</td>`
}
html += '</tr>'
}
html += '</table>'
} else {
const tag = getHtmlTag(element.type)
const processedContent = processContent(element.content)
html += `<${tag}>${processedContent}</${tag}>\n`
}
}
html += '</body></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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
function processContent(content: string): string {
// Convert markdown-style bold to HTML
return escapeHtml(content).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
}
// =============================================================================
// 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