// ============================================================================= // TOM Generator PDF Export // Export TOMs to PDF format // ============================================================================= import { TOMGeneratorState, DerivedTOM, CONTROL_CATEGORIES, } from '../types' import { getControlById } from '../controls/loader' // ============================================================================= // TYPES // ============================================================================= export interface PDFExportOptions { language: 'de' | 'en' includeNotApplicable: boolean includeEvidence: boolean includeGapAnalysis: boolean companyLogo?: string primaryColor?: string pageSize?: 'A4' | 'LETTER' orientation?: 'portrait' | 'landscape' } const DEFAULT_OPTIONS: PDFExportOptions = { language: 'de', includeNotApplicable: false, includeEvidence: true, includeGapAnalysis: true, primaryColor: '#1a56db', pageSize: 'A4', orientation: 'portrait', } // ============================================================================= // PDF CONTENT STRUCTURE // ============================================================================= export interface PDFSection { type: 'title' | 'heading' | 'subheading' | 'paragraph' | 'table' | 'list' | 'pagebreak' content?: string items?: string[] table?: { headers: string[] rows: string[][] } style?: { color?: string fontSize?: number bold?: boolean italic?: boolean align?: 'left' | 'center' | 'right' } } /** * Generate PDF content structure for TOMs */ export function generatePDFContent( state: TOMGeneratorState, options: Partial = {} ): PDFSection[] { const opts = { ...DEFAULT_OPTIONS, ...options } const sections: PDFSection[] = [] // Title page sections.push({ type: 'title', content: opts.language === 'de' ? 'Technische und Organisatorische Maßnahmen (TOMs)' : 'Technical and Organizational Measures (TOMs)', style: { color: opts.primaryColor, fontSize: 24, bold: true, align: 'center' }, }) sections.push({ type: 'paragraph', content: opts.language === 'de' ? 'gemäß Art. 32 DSGVO' : 'according to Art. 32 GDPR', style: { fontSize: 14, align: 'center' }, }) // Company information if (state.companyProfile) { sections.push({ type: 'paragraph', content: state.companyProfile.name, style: { fontSize: 16, bold: true, align: 'center' }, }) sections.push({ type: 'paragraph', content: `${opts.language === 'de' ? 'Branche' : 'Industry'}: ${Array.isArray(state.companyProfile.industry) ? state.companyProfile.industry.join(', ') : state.companyProfile.industry}`, style: { align: 'center' }, }) sections.push({ type: 'paragraph', content: `${opts.language === 'de' ? 'Stand' : 'Date'}: ${new Date().toLocaleDateString(opts.language === 'de' ? 'de-DE' : 'en-US')}`, style: { align: 'center' }, }) } sections.push({ type: 'pagebreak' }) // Table of Contents sections.push({ type: 'heading', content: opts.language === 'de' ? 'Inhaltsverzeichnis' : 'Table of Contents', style: { color: opts.primaryColor }, }) const tocItems = [ opts.language === 'de' ? '1. Zusammenfassung' : '1. Summary', opts.language === 'de' ? '2. Schutzbedarf' : '2. Protection Level', opts.language === 'de' ? '3. Maßnahmenübersicht' : '3. Measures Overview', ] let sectionNum = 4 for (const category of CONTROL_CATEGORIES) { const categoryTOMs = state.derivedTOMs.filter((tom) => { const control = getControlById(tom.controlId) return control?.category === category.id && (opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE') }) if (categoryTOMs.length > 0) { tocItems.push(`${sectionNum}. ${category.name[opts.language]}`) sectionNum++ } } if (opts.includeGapAnalysis && state.gapAnalysis) { tocItems.push(`${sectionNum}. ${opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis'}`) } sections.push({ type: 'list', items: tocItems, }) sections.push({ type: 'pagebreak' }) // Executive Summary sections.push({ type: 'heading', content: opts.language === 'de' ? '1. Zusammenfassung' : '1. Summary', style: { color: opts.primaryColor }, }) const totalTOMs = state.derivedTOMs.length const requiredTOMs = state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length const implementedTOMs = state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length sections.push({ type: 'paragraph', content: opts.language === 'de' ? `Dieses Dokument beschreibt die technischen und organisatorischen Maßnahmen (TOMs) gemäß Art. 32 DSGVO. Insgesamt wurden ${totalTOMs} Kontrollen bewertet, davon ${requiredTOMs} als erforderlich eingestuft. Aktuell sind ${implementedTOMs} Maßnahmen vollständig umgesetzt.` : `This document describes the technical and organizational measures (TOMs) according to Art. 32 GDPR. A total of ${totalTOMs} controls were evaluated, of which ${requiredTOMs} are classified as required. Currently, ${implementedTOMs} measures are fully implemented.`, }) // Summary statistics table sections.push({ type: 'table', table: { headers: opts.language === 'de' ? ['Kategorie', 'Anzahl', 'Erforderlich', 'Umgesetzt'] : ['Category', 'Count', 'Required', 'Implemented'], rows: generateCategorySummary(state.derivedTOMs, opts), }, }) // Protection Level sections.push({ type: 'heading', content: opts.language === 'de' ? '2. Schutzbedarf' : '2. Protection Level', style: { color: opts.primaryColor }, }) if (state.riskProfile) { sections.push({ type: 'paragraph', content: opts.language === 'de' ? `Der ermittelte Schutzbedarf beträgt: **${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}**` : `The determined protection level is: **${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}**`, }) sections.push({ type: 'table', table: { headers: opts.language === 'de' ? ['Schutzziel', 'Bewertung (1-5)', 'Bedeutung'] : ['Protection Goal', 'Rating (1-5)', 'Meaning'], rows: [ [ opts.language === 'de' ? 'Vertraulichkeit' : 'Confidentiality', String(state.riskProfile.ciaAssessment.confidentiality), getCIAMeaning(state.riskProfile.ciaAssessment.confidentiality, opts.language), ], [ opts.language === 'de' ? 'Integrität' : 'Integrity', String(state.riskProfile.ciaAssessment.integrity), getCIAMeaning(state.riskProfile.ciaAssessment.integrity, opts.language), ], [ opts.language === 'de' ? 'Verfügbarkeit' : 'Availability', String(state.riskProfile.ciaAssessment.availability), getCIAMeaning(state.riskProfile.ciaAssessment.availability, opts.language), ], ], }, }) if (state.riskProfile.dsfaRequired) { sections.push({ type: 'paragraph', content: opts.language === 'de' ? '⚠️ HINWEIS: Aufgrund der Verarbeitung ist eine Datenschutz-Folgenabschätzung (DSFA) nach Art. 35 DSGVO erforderlich.' : '⚠️ NOTE: Due to the processing, a Data Protection Impact Assessment (DPIA) according to Art. 35 GDPR is required.', style: { bold: true, color: '#dc2626' }, }) } } // Measures Overview sections.push({ type: 'heading', content: opts.language === 'de' ? '3. Maßnahmenübersicht' : '3. Measures Overview', style: { color: opts.primaryColor }, }) sections.push({ type: 'table', table: { headers: opts.language === 'de' ? ['ID', 'Maßnahme', 'Anwendbarkeit', 'Status'] : ['ID', 'Measure', 'Applicability', 'Status'], rows: state.derivedTOMs .filter((tom) => opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE') .map((tom) => [ tom.controlId, tom.name, formatApplicability(tom.applicability, opts.language), formatImplementationStatus(tom.implementationStatus, opts.language), ]), }, }) // Detailed sections by category let currentSection = 4 for (const category of CONTROL_CATEGORIES) { const categoryTOMs = state.derivedTOMs.filter((tom) => { const control = getControlById(tom.controlId) return control?.category === category.id && (opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE') }) if (categoryTOMs.length === 0) continue sections.push({ type: 'pagebreak' }) sections.push({ type: 'heading', content: `${currentSection}. ${category.name[opts.language]}`, style: { color: opts.primaryColor }, }) sections.push({ type: 'paragraph', content: `${opts.language === 'de' ? 'Rechtsgrundlage' : 'Legal Basis'}: ${category.gdprReference}`, style: { italic: true }, }) for (const tom of categoryTOMs) { sections.push({ type: 'subheading', content: `${tom.controlId}: ${tom.name}`, }) sections.push({ type: 'paragraph', content: tom.aiGeneratedDescription || tom.description, }) sections.push({ type: 'list', items: [ `${opts.language === 'de' ? 'Typ' : 'Type'}: ${formatType(getControlById(tom.controlId)?.type || 'TECHNICAL', opts.language)}`, `${opts.language === 'de' ? 'Anwendbarkeit' : 'Applicability'}: ${formatApplicability(tom.applicability, opts.language)}`, `${opts.language === 'de' ? 'Begründung' : 'Reason'}: ${tom.applicabilityReason}`, `${opts.language === 'de' ? 'Umsetzungsstatus' : 'Implementation Status'}: ${formatImplementationStatus(tom.implementationStatus, opts.language)}`, ...(tom.responsiblePerson ? [`${opts.language === 'de' ? 'Verantwortlich' : 'Responsible'}: ${tom.responsiblePerson}`] : []), ...(opts.includeEvidence && tom.linkedEvidence.length > 0 ? [`${opts.language === 'de' ? 'Verknüpfte Nachweise' : 'Linked Evidence'}: ${tom.linkedEvidence.length}`] : []), ], }) } currentSection++ } // Gap Analysis if (opts.includeGapAnalysis && state.gapAnalysis) { sections.push({ type: 'pagebreak' }) sections.push({ type: 'heading', content: `${currentSection}. ${opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis'}`, style: { color: opts.primaryColor }, }) sections.push({ type: 'paragraph', content: opts.language === 'de' ? `Gesamtscore: ${state.gapAnalysis.overallScore}%` : `Overall Score: ${state.gapAnalysis.overallScore}%`, style: { fontSize: 16, bold: true }, }) if (state.gapAnalysis.missingControls.length > 0) { sections.push({ type: 'subheading', content: opts.language === 'de' ? 'Fehlende Maßnahmen' : 'Missing Measures', }) sections.push({ type: 'table', table: { headers: opts.language === 'de' ? ['ID', 'Maßnahme', 'Priorität'] : ['ID', 'Measure', 'Priority'], rows: state.gapAnalysis.missingControls.map((mc) => { const control = getControlById(mc.controlId) return [ mc.controlId, control?.name[opts.language] || 'Unknown', mc.priority, ] }), }, }) } if (state.gapAnalysis.recommendations.length > 0) { sections.push({ type: 'subheading', content: opts.language === 'de' ? 'Empfehlungen' : 'Recommendations', }) sections.push({ type: 'list', items: state.gapAnalysis.recommendations, }) } } // Footer sections.push({ type: 'paragraph', content: opts.language === 'de' ? `Generiert am ${new Date().toLocaleDateString('de-DE')} mit dem TOM Generator` : `Generated on ${new Date().toLocaleDateString('en-US')} with the TOM Generator`, style: { italic: true, align: 'center', fontSize: 10 }, }) return sections } // ============================================================================= // HELPER FUNCTIONS // ============================================================================= function generateCategorySummary( toms: DerivedTOM[], opts: PDFExportOptions ): string[][] { const summary: string[][] = [] for (const category of CONTROL_CATEGORIES) { const categoryTOMs = toms.filter((tom) => { const control = getControlById(tom.controlId) return control?.category === category.id }) if (categoryTOMs.length === 0) continue const required = categoryTOMs.filter((t) => t.applicability === 'REQUIRED').length const implemented = categoryTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length summary.push([ category.name[opts.language], String(categoryTOMs.length), String(required), String(implemented), ]) } return summary } 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', en: 'Partial' }, 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: 'N/A', en: 'N/A' }, } return apps[applicability]?.[language] || applicability } function getCIAMeaning(rating: number, language: 'de' | 'en'): string { const meanings: Record> = { 1: { de: 'Sehr gering', en: 'Very Low' }, 2: { de: 'Gering', en: 'Low' }, 3: { de: 'Mittel', en: 'Medium' }, 4: { de: 'Hoch', en: 'High' }, 5: { de: 'Sehr hoch', en: 'Very High' }, } return meanings[rating]?.[language] || String(rating) } // ============================================================================= // PDF BLOB GENERATION // Note: For production, use jspdf or pdfmake library // ============================================================================= /** * Generate a PDF file as a Blob * This is a placeholder - in production, use jspdf or similar library */ export async function generatePDFBlob( state: TOMGeneratorState, options: Partial = {} ): Promise { const content = generatePDFContent(state, options) // Convert to simple text-based content for now // In production, use jspdf library const textContent = content .map((section) => { switch (section.type) { case 'title': return `\n\n${'='.repeat(60)}\n${section.content}\n${'='.repeat(60)}\n` case 'heading': return `\n\n${section.content}\n${'-'.repeat(40)}\n` case 'subheading': return `\n${section.content}\n` case 'paragraph': return `${section.content}\n` case 'list': return section.items?.map((item) => ` • ${item}`).join('\n') + '\n' case 'table': if (section.table) { const headerLine = section.table.headers.join(' | ') const separator = '-'.repeat(headerLine.length) const rows = section.table.rows.map((row) => row.join(' | ')).join('\n') return `\n${headerLine}\n${separator}\n${rows}\n` } return '' case 'pagebreak': return '\n\n' + '='.repeat(60) + '\n\n' default: return '' } }) .join('') return new Blob([textContent], { type: 'application/pdf' }) } // ============================================================================= // FILENAME GENERATION // ============================================================================= /** * Generate a filename for the PDF export */ export function generatePDFFilename( 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}.pdf` } // Types are exported at their definition site above