Multi-Branche-Auswahl im CompanyProfile, erweiterte allowed-facts fuer Drafting Engine, Demo-Daten und TOM-Generator Anpassungen. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
526 lines
16 KiB
TypeScript
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: ${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<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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
}
|
|
|
|
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
|