- tom-generator/export/zip.ts: extract private helpers to zip-helpers.ts (544→342 LOC) - tom-generator/export/docx.ts: extract private helpers to docx-helpers.ts (525→378 LOC) - tom-generator/export/pdf.ts: extract private helpers to pdf-helpers.ts (517→446 LOC) - tom-generator/demo-data/index.ts: extract DEMO_RISK_PROFILES + DEMO_EVIDENCE_DOCUMENTS to demo-data-part2.ts (518→360 LOC) - einwilligungen/generator/privacy-policy-sections.ts: extract sections 5-7 to part2 (559→313 LOC) - einwilligungen/export/pdf.ts: extract HTML/CSS helpers to pdf-helpers.ts (505→296 LOC) - vendor-compliance/context.tsx: extract API action hooks to context-actions.tsx (509→286 LOC) All originals re-export from sibling files — zero consumer import changes needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
379 lines
12 KiB
TypeScript
379 lines
12 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'
|
|
import {
|
|
groupTOMsByCategory,
|
|
formatRole,
|
|
formatProtectionLevel,
|
|
formatType,
|
|
formatImplementationStatus,
|
|
formatApplicability,
|
|
generateHTMLFromContent,
|
|
} from './docx-helpers'
|
|
|
|
// =============================================================================
|
|
// 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
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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
|