refactor(admin-compliance): split 7 oversized files under 500 LOC hard cap (batch 3)

- 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>
This commit is contained in:
Sharang Parnerkar
2026-04-18 00:43:41 +02:00
parent feedeb052f
commit 7d8e5667c9
14 changed files with 1460 additions and 1312 deletions

View File

@@ -0,0 +1,168 @@
// =============================================================================
// TOM Generator Demo Data (Part 2)
// DEMO_RISK_PROFILES and DEMO_EVIDENCE_DOCUMENTS
// Extracted from index.ts to stay under 500 LOC hard cap
// =============================================================================
import {
RiskProfile,
EvidenceDocument,
} from '../types'
// =============================================================================
// DEMO RISK PROFILES
// =============================================================================
export const DEMO_RISK_PROFILES: Record<string, RiskProfile> = {
saas: {
ciaAssessment: {
confidentiality: 3,
integrity: 3,
availability: 4,
justification: 'Als SaaS-Anbieter ist die Verfügbarkeit kritisch für unsere Kunden. Vertraulichkeit und Integrität sind wichtig aufgrund der verarbeiteten Geschäftsdaten.',
},
protectionLevel: 'HIGH',
specialRisks: ['Cloud-Abhängigkeit', 'Multi-Mandanten-Umgebung'],
regulatoryRequirements: ['DSGVO', 'Kundenvorgaben'],
hasHighRiskProcessing: false,
dsfaRequired: false,
},
healthcare: {
ciaAssessment: {
confidentiality: 5,
integrity: 5,
availability: 4,
justification: 'Gesundheitsdaten erfordern höchsten Schutz. Fehlerhafte Daten können Patientensicherheit gefährden.',
},
protectionLevel: 'VERY_HIGH',
specialRisks: ['Gesundheitsdaten', 'Minderjährige', 'Telemedizin'],
regulatoryRequirements: ['DSGVO', 'SGB', 'MDR'],
hasHighRiskProcessing: true,
dsfaRequired: true,
},
enterprise: {
ciaAssessment: {
confidentiality: 4,
integrity: 5,
availability: 5,
justification: 'Finanzdienstleistungen erfordern höchste Integrität und Verfügbarkeit. Vertraulichkeit ist kritisch für Kundendaten und Transaktionen.',
},
protectionLevel: 'VERY_HIGH',
specialRisks: ['Finanztransaktionen', 'Regulatorische Auflagen', 'Cyber-Risiken'],
regulatoryRequirements: ['DSGVO', 'MaRisk', 'BAIT', 'PSD2'],
hasHighRiskProcessing: true,
dsfaRequired: true,
},
}
// =============================================================================
// DEMO EVIDENCE DOCUMENTS
// =============================================================================
export const DEMO_EVIDENCE_DOCUMENTS: EvidenceDocument[] = [
{
id: 'demo-evidence-1',
filename: 'iso27001-certificate.pdf',
originalName: 'ISO 27001 Zertifikat.pdf',
mimeType: 'application/pdf',
size: 245678,
uploadedAt: new Date('2025-01-15'),
uploadedBy: 'admin@company.de',
documentType: 'CERTIFICATE',
detectedType: 'CERTIFICATE',
hash: 'sha256:abc123def456',
validFrom: new Date('2024-06-01'),
validUntil: new Date('2027-05-31'),
linkedControlIds: ['TOM-RV-04', 'TOM-AZ-01'],
aiAnalysis: {
summary: 'ISO 27001:2022 Zertifikat bestätigt die Implementierung eines Informationssicherheits-Managementsystems.',
extractedClauses: [
{
id: 'clause-1',
text: 'Zertifiziert nach ISO/IEC 27001:2022',
type: 'certification',
relatedControlId: 'TOM-RV-04',
},
],
applicableControls: ['TOM-RV-04', 'TOM-AZ-01', 'TOM-RV-01'],
gaps: [],
confidence: 0.95,
analyzedAt: new Date('2025-01-15'),
},
status: 'VERIFIED',
},
{
id: 'demo-evidence-2',
filename: 'passwort-richtlinie.pdf',
originalName: 'Passwortrichtlinie v2.1.pdf',
mimeType: 'application/pdf',
size: 128456,
uploadedAt: new Date('2025-01-10'),
uploadedBy: 'admin@company.de',
documentType: 'POLICY',
detectedType: 'POLICY',
hash: 'sha256:xyz789abc012',
validFrom: new Date('2024-09-01'),
validUntil: null,
linkedControlIds: ['TOM-ADM-02'],
aiAnalysis: {
summary: 'Interne Passwortrichtlinie definiert Anforderungen an Passwortlänge, Komplexität und Wechselintervalle.',
extractedClauses: [
{
id: 'clause-1',
text: 'Mindestlänge 12 Zeichen, Groß-/Kleinbuchstaben, Zahlen und Sonderzeichen erforderlich',
type: 'password-policy',
relatedControlId: 'TOM-ADM-02',
},
{
id: 'clause-2',
text: 'Passwörter müssen alle 90 Tage geändert werden',
type: 'password-policy',
relatedControlId: 'TOM-ADM-02',
},
],
applicableControls: ['TOM-ADM-02'],
gaps: ['Keine Regelung zur Passwort-Historie gefunden'],
confidence: 0.85,
analyzedAt: new Date('2025-01-10'),
},
status: 'ANALYZED',
},
{
id: 'demo-evidence-3',
filename: 'aws-avv.pdf',
originalName: 'AWS Data Processing Addendum.pdf',
mimeType: 'application/pdf',
size: 456789,
uploadedAt: new Date('2025-01-05'),
uploadedBy: 'admin@company.de',
documentType: 'AVV',
detectedType: 'DPA',
hash: 'sha256:qwe123rty456',
validFrom: new Date('2024-01-01'),
validUntil: null,
linkedControlIds: ['TOM-OR-01', 'TOM-OR-02'],
aiAnalysis: {
summary: 'AWS Data Processing Addendum regelt die Auftragsverarbeitung durch AWS als Unterauftragsverarbeiter.',
extractedClauses: [
{
id: 'clause-1',
text: 'AWS verpflichtet sich zur Einhaltung der DSGVO-Anforderungen',
type: 'data-processing',
relatedControlId: 'TOM-OR-01',
},
{
id: 'clause-2',
text: 'Jährliche SOC 2 und ISO 27001 Audits werden durchgeführt',
type: 'audit',
relatedControlId: 'TOM-OR-02',
},
],
applicableControls: ['TOM-OR-01', 'TOM-OR-02', 'TOM-OR-04'],
gaps: [],
confidence: 0.9,
analyzedAt: new Date('2025-01-05'),
},
status: 'VERIFIED',
},
]

View File

@@ -9,13 +9,13 @@ import {
DataProfile,
ArchitectureProfile,
SecurityProfile,
RiskProfile,
EvidenceDocument,
DerivedTOM,
GapAnalysisResult,
TOM_GENERATOR_STEPS,
} from '../types'
import { getTOMRulesEngine } from '../rules-engine'
import { DEMO_RISK_PROFILES, DEMO_EVIDENCE_DOCUMENTS } from './demo-data-part2'
// Re-export risk profiles and evidence from part2 for backward compatibility
export { DEMO_RISK_PROFILES, DEMO_EVIDENCE_DOCUMENTS } from './demo-data-part2'
// =============================================================================
// DEMO COMPANY PROFILES
@@ -216,164 +216,6 @@ export const DEMO_SECURITY_PROFILES: Record<string, SecurityProfile> = {
},
}
// =============================================================================
// DEMO RISK PROFILES
// =============================================================================
export const DEMO_RISK_PROFILES: Record<string, RiskProfile> = {
saas: {
ciaAssessment: {
confidentiality: 3,
integrity: 3,
availability: 4,
justification: 'Als SaaS-Anbieter ist die Verfügbarkeit kritisch für unsere Kunden. Vertraulichkeit und Integrität sind wichtig aufgrund der verarbeiteten Geschäftsdaten.',
},
protectionLevel: 'HIGH',
specialRisks: ['Cloud-Abhängigkeit', 'Multi-Mandanten-Umgebung'],
regulatoryRequirements: ['DSGVO', 'Kundenvorgaben'],
hasHighRiskProcessing: false,
dsfaRequired: false,
},
healthcare: {
ciaAssessment: {
confidentiality: 5,
integrity: 5,
availability: 4,
justification: 'Gesundheitsdaten erfordern höchsten Schutz. Fehlerhafte Daten können Patientensicherheit gefährden.',
},
protectionLevel: 'VERY_HIGH',
specialRisks: ['Gesundheitsdaten', 'Minderjährige', 'Telemedizin'],
regulatoryRequirements: ['DSGVO', 'SGB', 'MDR'],
hasHighRiskProcessing: true,
dsfaRequired: true,
},
enterprise: {
ciaAssessment: {
confidentiality: 4,
integrity: 5,
availability: 5,
justification: 'Finanzdienstleistungen erfordern höchste Integrität und Verfügbarkeit. Vertraulichkeit ist kritisch für Kundendaten und Transaktionen.',
},
protectionLevel: 'VERY_HIGH',
specialRisks: ['Finanztransaktionen', 'Regulatorische Auflagen', 'Cyber-Risiken'],
regulatoryRequirements: ['DSGVO', 'MaRisk', 'BAIT', 'PSD2'],
hasHighRiskProcessing: true,
dsfaRequired: true,
},
}
// =============================================================================
// DEMO EVIDENCE DOCUMENTS
// =============================================================================
export const DEMO_EVIDENCE_DOCUMENTS: EvidenceDocument[] = [
{
id: 'demo-evidence-1',
filename: 'iso27001-certificate.pdf',
originalName: 'ISO 27001 Zertifikat.pdf',
mimeType: 'application/pdf',
size: 245678,
uploadedAt: new Date('2025-01-15'),
uploadedBy: 'admin@company.de',
documentType: 'CERTIFICATE',
detectedType: 'CERTIFICATE',
hash: 'sha256:abc123def456',
validFrom: new Date('2024-06-01'),
validUntil: new Date('2027-05-31'),
linkedControlIds: ['TOM-RV-04', 'TOM-AZ-01'],
aiAnalysis: {
summary: 'ISO 27001:2022 Zertifikat bestätigt die Implementierung eines Informationssicherheits-Managementsystems.',
extractedClauses: [
{
id: 'clause-1',
text: 'Zertifiziert nach ISO/IEC 27001:2022',
type: 'certification',
relatedControlId: 'TOM-RV-04',
},
],
applicableControls: ['TOM-RV-04', 'TOM-AZ-01', 'TOM-RV-01'],
gaps: [],
confidence: 0.95,
analyzedAt: new Date('2025-01-15'),
},
status: 'VERIFIED',
},
{
id: 'demo-evidence-2',
filename: 'passwort-richtlinie.pdf',
originalName: 'Passwortrichtlinie v2.1.pdf',
mimeType: 'application/pdf',
size: 128456,
uploadedAt: new Date('2025-01-10'),
uploadedBy: 'admin@company.de',
documentType: 'POLICY',
detectedType: 'POLICY',
hash: 'sha256:xyz789abc012',
validFrom: new Date('2024-09-01'),
validUntil: null,
linkedControlIds: ['TOM-ADM-02'],
aiAnalysis: {
summary: 'Interne Passwortrichtlinie definiert Anforderungen an Passwortlänge, Komplexität und Wechselintervalle.',
extractedClauses: [
{
id: 'clause-1',
text: 'Mindestlänge 12 Zeichen, Groß-/Kleinbuchstaben, Zahlen und Sonderzeichen erforderlich',
type: 'password-policy',
relatedControlId: 'TOM-ADM-02',
},
{
id: 'clause-2',
text: 'Passwörter müssen alle 90 Tage geändert werden',
type: 'password-policy',
relatedControlId: 'TOM-ADM-02',
},
],
applicableControls: ['TOM-ADM-02'],
gaps: ['Keine Regelung zur Passwort-Historie gefunden'],
confidence: 0.85,
analyzedAt: new Date('2025-01-10'),
},
status: 'ANALYZED',
},
{
id: 'demo-evidence-3',
filename: 'aws-avv.pdf',
originalName: 'AWS Data Processing Addendum.pdf',
mimeType: 'application/pdf',
size: 456789,
uploadedAt: new Date('2025-01-05'),
uploadedBy: 'admin@company.de',
documentType: 'AVV',
detectedType: 'DPA',
hash: 'sha256:qwe123rty456',
validFrom: new Date('2024-01-01'),
validUntil: null,
linkedControlIds: ['TOM-OR-01', 'TOM-OR-02'],
aiAnalysis: {
summary: 'AWS Data Processing Addendum regelt die Auftragsverarbeitung durch AWS als Unterauftragsverarbeiter.',
extractedClauses: [
{
id: 'clause-1',
text: 'AWS verpflichtet sich zur Einhaltung der DSGVO-Anforderungen',
type: 'data-processing',
relatedControlId: 'TOM-OR-01',
},
{
id: 'clause-2',
text: 'Jährliche SOC 2 und ISO 27001 Audits werden durchgeführt',
type: 'audit',
relatedControlId: 'TOM-OR-02',
},
],
applicableControls: ['TOM-OR-01', 'TOM-OR-02', 'TOM-OR-04'],
gaps: [],
confidence: 0.9,
analyzedAt: new Date('2025-01-05'),
},
status: 'VERIFIED',
},
]
// =============================================================================
// DEMO STATE GENERATOR
// =============================================================================

View File

@@ -0,0 +1,165 @@
// =============================================================================
// TOM Generator DOCX Export - Helper Functions
// Private helpers extracted from docx.ts to stay under 500 LOC hard cap
// =============================================================================
import { DerivedTOM, ControlCategory } from '../types'
import { getControlById } from '../controls/loader'
import type { DOCXExportOptions, DocxTableRow } from './docx'
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
export 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
}
export 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
}
export 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
}
export 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
}
export 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
}
export 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
}
export function generateHTMLFromContent(
content: Array<{ type: string; content?: string; headers?: string[]; rows?: DocxTableRow[] }>,
options: Partial<DOCXExportOptions>
): string {
const DEFAULT_COLOR = '#1a56db'
const primaryColor = options.primaryColor || DEFAULT_COLOR
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: ${primaryColor}; border-bottom: 2px solid ${primaryColor}; }
h2 { font-size: 18pt; color: ${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: ${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' && element.headers && element.rows) {
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
}
export 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'
}
}
export function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
export function processContent(content: string): string {
// Convert markdown-style bold to HTML
return escapeHtml(content).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
}

View File

@@ -10,6 +10,15 @@ import {
CONTROL_CATEGORIES,
} from '../types'
import { getControlById, getCategoryMetadata } from '../controls/loader'
import {
groupTOMsByCategory,
formatRole,
formatProtectionLevel,
formatType,
formatImplementationStatus,
formatApplicability,
generateHTMLFromContent,
} from './docx-helpers'
// =============================================================================
// TYPES
@@ -320,78 +329,6 @@ export function generateDOCXContent(
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
@@ -421,90 +358,6 @@ export async function generateDOCXBlob(
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
// =============================================================================

View File

@@ -0,0 +1,87 @@
// =============================================================================
// TOM Generator PDF Export - Helper Functions
// Private helpers extracted from pdf.ts to stay under 500 LOC hard cap
// =============================================================================
import { DerivedTOM, CONTROL_CATEGORIES } from '../types'
import { getControlById } from '../controls/loader'
import type { PDFExportOptions } from './pdf'
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
export 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
}
export 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
}
export 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
}
export 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
}
export 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
}
export 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)
}

View File

@@ -9,6 +9,14 @@ import {
CONTROL_CATEGORIES,
} from '../types'
import { getControlById } from '../controls/loader'
import {
generateCategorySummary,
formatProtectionLevel,
formatType,
formatImplementationStatus,
formatApplicability,
getCIAMeaning,
} from './pdf-helpers'
// =============================================================================
// TYPES
@@ -369,85 +377,6 @@ export function generatePDFContent(
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

View File

@@ -0,0 +1,219 @@
// =============================================================================
// TOM Generator ZIP Export - Helper Functions
// Private helpers extracted from zip.ts to stay under 500 LOC hard cap
// =============================================================================
import { TOMGeneratorState, DerivedTOM } from '../types'
import { getControlById } from '../controls/loader'
import type { ZIPExportOptions } from './zip'
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
export function generateReadme(
state: TOMGeneratorState,
opts: ZIPExportOptions
): string {
const date = new Date().toISOString().split('T')[0]
const lang = opts.language
return `# TOM Export Package
${lang === 'de' ? 'Exportiert am' : 'Exported on'}: ${date}
${lang === 'de' ? 'Unternehmen' : 'Company'}: ${state.companyProfile?.name || 'N/A'}
## ${lang === 'de' ? 'Inhalt' : 'Contents'}
### /data
- **profiles/** - ${lang === 'de' ? 'Profilinformationen (Unternehmen, Daten, Architektur, Sicherheit, Risiko)' : 'Profile information (company, data, architecture, security, risk)'}
- **toms/** - ${lang === 'de' ? 'Abgeleitete TOMs und Zusammenfassungen' : 'Derived TOMs and summaries'}
- **evidence/** - ${lang === 'de' ? 'Nachweisdokumente und Zuordnungen' : 'Evidence documents and mappings'}
- **gap-analysis/** - ${lang === 'de' ? 'Lückenanalyse und Empfehlungen' : 'Gap analysis and recommendations'}
### /reference
- **control-library/** - ${lang === 'de' ? 'Kontrollbibliothek mit allen 60+ Kontrollen' : 'Control library with all 60+ controls'}
### /documents
- **tom-summary.md** - ${lang === 'de' ? 'Zusammenfassung als Markdown' : 'Summary as Markdown'}
- **toms.csv** - ${lang === 'de' ? 'CSV für Tabellenimport' : 'CSV for spreadsheet import'}
## ${lang === 'de' ? 'Statistiken' : 'Statistics'}
- ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'}: ${state.derivedTOMs.length}
- ${lang === 'de' ? 'Erforderlich' : 'Required'}: ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length}
- ${lang === 'de' ? 'Umgesetzt' : 'Implemented'}: ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length}
- ${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}: ${state.riskProfile?.protectionLevel || 'N/A'}
${state.gapAnalysis ? `- ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}: ${state.gapAnalysis.overallScore}%` : ''}
---
${lang === 'de' ? 'Generiert mit dem TOM Generator' : 'Generated with TOM Generator'}
`
}
export function groupTOMsByCategory(
toms: DerivedTOM[]
): Map<string, DerivedTOM[]> {
const grouped = new Map<string, DerivedTOM[]>()
for (const tom of toms) {
const control = getControlById(tom.controlId)
if (!control) continue
const category = control.category
const existing: DerivedTOM[] = grouped.get(category) || []
existing.push(tom)
grouped.set(category, existing)
}
return grouped
}
export function generateImplementationSummary(
toms: Array<{ implementationStatus: string; applicability: string }>
): Record<string, number> {
return {
total: toms.length,
required: toms.filter((t) => t.applicability === 'REQUIRED').length,
recommended: toms.filter((t) => t.applicability === 'RECOMMENDED').length,
optional: toms.filter((t) => t.applicability === 'OPTIONAL').length,
notApplicable: toms.filter((t) => t.applicability === 'NOT_APPLICABLE').length,
implemented: toms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length,
partial: toms.filter((t) => t.implementationStatus === 'PARTIAL').length,
notImplemented: toms.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length,
}
}
export function groupEvidenceByControl(
documents: Array<{ id: string; linkedControlIds: string[] }>
): Map<string, string[]> {
const grouped = new Map<string, string[]>()
for (const doc of documents) {
for (const controlId of doc.linkedControlIds) {
const existing = grouped.get(controlId) || []
existing.push(doc.id)
grouped.set(controlId, existing)
}
}
return grouped
}
export function generateRecommendationsMarkdown(
recommendations: string[],
language: 'de' | 'en'
): string {
const title = language === 'de' ? 'Empfehlungen' : 'Recommendations'
return `# ${title}
${recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\n\n')}
---
${language === 'de' ? 'Generiert am' : 'Generated on'} ${new Date().toISOString().split('T')[0]}
`
}
export function generateMarkdownSummary(
state: TOMGeneratorState,
opts: ZIPExportOptions
): string {
const lang = opts.language
const date = new Date().toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US')
let md = `# ${lang === 'de' ? 'Technische und Organisatorische Maßnahmen' : 'Technical and Organizational Measures'}
**${lang === 'de' ? 'Unternehmen' : 'Company'}:** ${state.companyProfile?.name || 'N/A'}
**${lang === 'de' ? 'Stand' : 'Date'}:** ${date}
**${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}:** ${state.riskProfile?.protectionLevel || 'N/A'}
## ${lang === 'de' ? 'Zusammenfassung' : 'Summary'}
| ${lang === 'de' ? 'Metrik' : 'Metric'} | ${lang === 'de' ? 'Wert' : 'Value'} |
|--------|-------|
| ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'} | ${state.derivedTOMs.length} |
| ${lang === 'de' ? 'Erforderlich' : 'Required'} | ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length} |
| ${lang === 'de' ? 'Umgesetzt' : 'Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length} |
| ${lang === 'de' ? 'Teilweise umgesetzt' : 'Partially Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'PARTIAL').length} |
| ${lang === 'de' ? 'Nicht umgesetzt' : 'Not Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length} |
`
if (state.gapAnalysis) {
md += `
## ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}
**${state.gapAnalysis.overallScore}%**
`
}
// Add required TOMs table
const requiredTOMs = state.derivedTOMs.filter(
(t) => t.applicability === 'REQUIRED'
)
if (requiredTOMs.length > 0) {
md += `
## ${lang === 'de' ? 'Erforderliche Maßnahmen' : 'Required Measures'}
| ID | ${lang === 'de' ? 'Maßnahme' : 'Measure'} | Status |
|----|----------|--------|
${requiredTOMs.map((tom) => `| ${tom.controlId} | ${tom.name} | ${formatStatus(tom.implementationStatus, lang)} |`).join('\n')}
`
}
return md
}
export function generateCSV(
toms: Array<{
controlId: string
name: string
description: string
applicability: string
implementationStatus: string
responsiblePerson: string | null
}>,
opts: ZIPExportOptions
): string {
const lang = opts.language
const headers = lang === 'de'
? ['ID', 'Name', 'Beschreibung', 'Anwendbarkeit', 'Status', 'Verantwortlich']
: ['ID', 'Name', 'Description', 'Applicability', 'Status', 'Responsible']
const rows = toms.map((tom) => [
tom.controlId,
escapeCSV(tom.name),
escapeCSV(tom.description),
tom.applicability,
tom.implementationStatus,
tom.responsiblePerson || '',
])
return [
headers.join(','),
...rows.map((row) => row.join(',')),
].join('\n')
}
export function escapeCSV(value: string): string {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`
}
return value
}
export function formatStatus(status: string, lang: '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]?.[lang] || status
}

View File

@@ -6,7 +6,16 @@
import { TOMGeneratorState, DerivedTOM, EvidenceDocument } from '../types'
import { generateDOCXContent, DOCXExportOptions } from './docx'
import { generatePDFContent, PDFExportOptions } from './pdf'
import { getControlById, getAllControls, getLibraryMetadata } from '../controls/loader'
import { getAllControls, getLibraryMetadata } from '../controls/loader'
import {
generateReadme,
groupTOMsByCategory,
generateImplementationSummary,
groupEvidenceByControl,
generateRecommendationsMarkdown,
generateMarkdownSummary,
generateCSV,
} from './zip-helpers'
// =============================================================================
// TYPES
@@ -267,217 +276,6 @@ export function generateZIPFiles(
return files
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function generateReadme(
state: TOMGeneratorState,
opts: ZIPExportOptions
): string {
const date = new Date().toISOString().split('T')[0]
const lang = opts.language
return `# TOM Export Package
${lang === 'de' ? 'Exportiert am' : 'Exported on'}: ${date}
${lang === 'de' ? 'Unternehmen' : 'Company'}: ${state.companyProfile?.name || 'N/A'}
## ${lang === 'de' ? 'Inhalt' : 'Contents'}
### /data
- **profiles/** - ${lang === 'de' ? 'Profilinformationen (Unternehmen, Daten, Architektur, Sicherheit, Risiko)' : 'Profile information (company, data, architecture, security, risk)'}
- **toms/** - ${lang === 'de' ? 'Abgeleitete TOMs und Zusammenfassungen' : 'Derived TOMs and summaries'}
- **evidence/** - ${lang === 'de' ? 'Nachweisdokumente und Zuordnungen' : 'Evidence documents and mappings'}
- **gap-analysis/** - ${lang === 'de' ? 'Lückenanalyse und Empfehlungen' : 'Gap analysis and recommendations'}
### /reference
- **control-library/** - ${lang === 'de' ? 'Kontrollbibliothek mit allen 60+ Kontrollen' : 'Control library with all 60+ controls'}
### /documents
- **tom-summary.md** - ${lang === 'de' ? 'Zusammenfassung als Markdown' : 'Summary as Markdown'}
- **toms.csv** - ${lang === 'de' ? 'CSV für Tabellenimport' : 'CSV for spreadsheet import'}
## ${lang === 'de' ? 'Statistiken' : 'Statistics'}
- ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'}: ${state.derivedTOMs.length}
- ${lang === 'de' ? 'Erforderlich' : 'Required'}: ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length}
- ${lang === 'de' ? 'Umgesetzt' : 'Implemented'}: ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length}
- ${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}: ${state.riskProfile?.protectionLevel || 'N/A'}
${state.gapAnalysis ? `- ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}: ${state.gapAnalysis.overallScore}%` : ''}
---
${lang === 'de' ? 'Generiert mit dem TOM Generator' : 'Generated with TOM Generator'}
`
}
function groupTOMsByCategory(
toms: DerivedTOM[]
): Map<string, DerivedTOM[]> {
const grouped = new Map<string, DerivedTOM[]>()
for (const tom of toms) {
const control = getControlById(tom.controlId)
if (!control) continue
const category = control.category
const existing: DerivedTOM[] = grouped.get(category) || []
existing.push(tom)
grouped.set(category, existing)
}
return grouped
}
function generateImplementationSummary(
toms: Array<{ implementationStatus: string; applicability: string }>
): Record<string, number> {
return {
total: toms.length,
required: toms.filter((t) => t.applicability === 'REQUIRED').length,
recommended: toms.filter((t) => t.applicability === 'RECOMMENDED').length,
optional: toms.filter((t) => t.applicability === 'OPTIONAL').length,
notApplicable: toms.filter((t) => t.applicability === 'NOT_APPLICABLE').length,
implemented: toms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length,
partial: toms.filter((t) => t.implementationStatus === 'PARTIAL').length,
notImplemented: toms.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length,
}
}
function groupEvidenceByControl(
documents: Array<{ id: string; linkedControlIds: string[] }>
): Map<string, string[]> {
const grouped = new Map<string, string[]>()
for (const doc of documents) {
for (const controlId of doc.linkedControlIds) {
const existing = grouped.get(controlId) || []
existing.push(doc.id)
grouped.set(controlId, existing)
}
}
return grouped
}
function generateRecommendationsMarkdown(
recommendations: string[],
language: 'de' | 'en'
): string {
const title = language === 'de' ? 'Empfehlungen' : 'Recommendations'
return `# ${title}
${recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\n\n')}
---
${language === 'de' ? 'Generiert am' : 'Generated on'} ${new Date().toISOString().split('T')[0]}
`
}
function generateMarkdownSummary(
state: TOMGeneratorState,
opts: ZIPExportOptions
): string {
const lang = opts.language
const date = new Date().toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US')
let md = `# ${lang === 'de' ? 'Technische und Organisatorische Maßnahmen' : 'Technical and Organizational Measures'}
**${lang === 'de' ? 'Unternehmen' : 'Company'}:** ${state.companyProfile?.name || 'N/A'}
**${lang === 'de' ? 'Stand' : 'Date'}:** ${date}
**${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}:** ${state.riskProfile?.protectionLevel || 'N/A'}
## ${lang === 'de' ? 'Zusammenfassung' : 'Summary'}
| ${lang === 'de' ? 'Metrik' : 'Metric'} | ${lang === 'de' ? 'Wert' : 'Value'} |
|--------|-------|
| ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'} | ${state.derivedTOMs.length} |
| ${lang === 'de' ? 'Erforderlich' : 'Required'} | ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length} |
| ${lang === 'de' ? 'Umgesetzt' : 'Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length} |
| ${lang === 'de' ? 'Teilweise umgesetzt' : 'Partially Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'PARTIAL').length} |
| ${lang === 'de' ? 'Nicht umgesetzt' : 'Not Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length} |
`
if (state.gapAnalysis) {
md += `
## ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}
**${state.gapAnalysis.overallScore}%**
`
}
// Add required TOMs table
const requiredTOMs = state.derivedTOMs.filter(
(t) => t.applicability === 'REQUIRED'
)
if (requiredTOMs.length > 0) {
md += `
## ${lang === 'de' ? 'Erforderliche Maßnahmen' : 'Required Measures'}
| ID | ${lang === 'de' ? 'Maßnahme' : 'Measure'} | Status |
|----|----------|--------|
${requiredTOMs.map((tom) => `| ${tom.controlId} | ${tom.name} | ${formatStatus(tom.implementationStatus, lang)} |`).join('\n')}
`
}
return md
}
function generateCSV(
toms: Array<{
controlId: string
name: string
description: string
applicability: string
implementationStatus: string
responsiblePerson: string | null
}>,
opts: ZIPExportOptions
): string {
const lang = opts.language
const headers = lang === 'de'
? ['ID', 'Name', 'Beschreibung', 'Anwendbarkeit', 'Status', 'Verantwortlich']
: ['ID', 'Name', 'Description', 'Applicability', 'Status', 'Responsible']
const rows = toms.map((tom) => [
tom.controlId,
escapeCSV(tom.name),
escapeCSV(tom.description),
tom.applicability,
tom.implementationStatus,
tom.responsiblePerson || '',
])
return [
headers.join(','),
...rows.map((row) => row.join(',')),
].join('\n')
}
function escapeCSV(value: string): string {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`
}
return value
}
function formatStatus(status: string, lang: '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]?.[lang] || status
}
// =============================================================================
// ZIP BLOB GENERATION
// Note: For production, use jszip library