The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
518 lines
17 KiB
TypeScript
518 lines
17 KiB
TypeScript
// =============================================================================
|
|
// 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<PDFExportOptions> = {}
|
|
): 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'}: ${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<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', en: 'Partial' },
|
|
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: 'N/A', en: 'N/A' },
|
|
}
|
|
return apps[applicability]?.[language] || applicability
|
|
}
|
|
|
|
function getCIAMeaning(rating: number, language: 'de' | 'en'): string {
|
|
const meanings: Record<number, Record<'de' | 'en', string>> = {
|
|
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<PDFExportOptions> = {}
|
|
): Promise<Blob> {
|
|
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
|