fix(admin-v2): Restore complete admin-v2 application

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>
This commit is contained in:
BreakPilot Dev
2026-02-08 23:40:15 -08:00
parent f28244753f
commit 660295e218
385 changed files with 138126 additions and 3079 deletions

View File

@@ -0,0 +1,525 @@
// =============================================================================
// 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: ${state.companyProfile.industry}`
: `Industry: ${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, '&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
// =============================================================================
/**
* 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

View File

@@ -0,0 +1,517 @@
// =============================================================================
// 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

View File

@@ -0,0 +1,544 @@
// =============================================================================
// TOM Generator ZIP Export
// Export complete TOM package as ZIP archive
// =============================================================================
import { TOMGeneratorState, DerivedTOM, EvidenceDocument } from '../types'
import { generateDOCXContent, DOCXExportOptions } from './docx'
import { generatePDFContent, PDFExportOptions } from './pdf'
import { getControlById, getAllControls, getLibraryMetadata } from '../controls/loader'
// =============================================================================
// TYPES
// =============================================================================
export interface ZIPExportOptions {
language: 'de' | 'en'
includeNotApplicable: boolean
includeEvidence: boolean
includeGapAnalysis: boolean
includeControlLibrary: boolean
includeRawData: boolean
formats: Array<'json' | 'docx' | 'pdf'>
}
const DEFAULT_OPTIONS: ZIPExportOptions = {
language: 'de',
includeNotApplicable: false,
includeEvidence: true,
includeGapAnalysis: true,
includeControlLibrary: true,
includeRawData: true,
formats: ['json', 'docx'],
}
// =============================================================================
// ZIP CONTENT STRUCTURE
// =============================================================================
export interface ZIPFileEntry {
path: string
content: string | Blob
mimeType: string
}
/**
* Generate all files for the ZIP archive
*/
export function generateZIPFiles(
state: TOMGeneratorState,
options: Partial<ZIPExportOptions> = {}
): ZIPFileEntry[] {
const opts = { ...DEFAULT_OPTIONS, ...options }
const files: ZIPFileEntry[] = []
// README
files.push({
path: 'README.md',
content: generateReadme(state, opts),
mimeType: 'text/markdown',
})
// State JSON
if (opts.includeRawData) {
files.push({
path: 'data/state.json',
content: JSON.stringify(state, null, 2),
mimeType: 'application/json',
})
}
// Profile data
files.push({
path: 'data/profiles/company-profile.json',
content: JSON.stringify(state.companyProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/data-profile.json',
content: JSON.stringify(state.dataProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/architecture-profile.json',
content: JSON.stringify(state.architectureProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/security-profile.json',
content: JSON.stringify(state.securityProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/risk-profile.json',
content: JSON.stringify(state.riskProfile, null, 2),
mimeType: 'application/json',
})
// Derived TOMs
files.push({
path: 'data/toms/derived-toms.json',
content: JSON.stringify(state.derivedTOMs, null, 2),
mimeType: 'application/json',
})
// TOMs by category
const tomsByCategory = groupTOMsByCategory(state.derivedTOMs)
for (const [category, toms] of tomsByCategory.entries()) {
files.push({
path: `data/toms/by-category/${category.toLowerCase()}.json`,
content: JSON.stringify(toms, null, 2),
mimeType: 'application/json',
})
}
// Required TOMs summary
const requiredTOMs = state.derivedTOMs.filter(
(tom) => tom.applicability === 'REQUIRED'
)
files.push({
path: 'data/toms/required-toms.json',
content: JSON.stringify(requiredTOMs, null, 2),
mimeType: 'application/json',
})
// Implementation status summary
const implementationSummary = generateImplementationSummary(state.derivedTOMs)
files.push({
path: 'data/toms/implementation-summary.json',
content: JSON.stringify(implementationSummary, null, 2),
mimeType: 'application/json',
})
// Evidence documents
if (opts.includeEvidence && state.documents.length > 0) {
files.push({
path: 'data/evidence/documents.json',
content: JSON.stringify(state.documents, null, 2),
mimeType: 'application/json',
})
// Evidence by control
const evidenceByControl = groupEvidenceByControl(state.documents)
files.push({
path: 'data/evidence/by-control.json',
content: JSON.stringify(Object.fromEntries(evidenceByControl), null, 2),
mimeType: 'application/json',
})
}
// Gap Analysis
if (opts.includeGapAnalysis && state.gapAnalysis) {
files.push({
path: 'data/gap-analysis/analysis.json',
content: JSON.stringify(state.gapAnalysis, null, 2),
mimeType: 'application/json',
})
// Missing controls details
if (state.gapAnalysis.missingControls.length > 0) {
files.push({
path: 'data/gap-analysis/missing-controls.json',
content: JSON.stringify(state.gapAnalysis.missingControls, null, 2),
mimeType: 'application/json',
})
}
// Recommendations
if (state.gapAnalysis.recommendations.length > 0) {
files.push({
path: 'data/gap-analysis/recommendations.md',
content: generateRecommendationsMarkdown(
state.gapAnalysis.recommendations,
opts.language
),
mimeType: 'text/markdown',
})
}
}
// Control Library
if (opts.includeControlLibrary) {
const controls = getAllControls()
const metadata = getLibraryMetadata()
files.push({
path: 'reference/control-library/metadata.json',
content: JSON.stringify(metadata, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'reference/control-library/all-controls.json',
content: JSON.stringify(controls, null, 2),
mimeType: 'application/json',
})
// Controls by category
for (const category of new Set(controls.map((c) => c.category))) {
const categoryControls = controls.filter((c) => c.category === category)
files.push({
path: `reference/control-library/by-category/${category.toLowerCase()}.json`,
content: JSON.stringify(categoryControls, null, 2),
mimeType: 'application/json',
})
}
}
// Export history
if (state.exports.length > 0) {
files.push({
path: 'data/exports/history.json',
content: JSON.stringify(state.exports, null, 2),
mimeType: 'application/json',
})
}
// DOCX content structure (if requested)
if (opts.formats.includes('docx')) {
const docxOptions: Partial<DOCXExportOptions> = {
language: opts.language,
includeNotApplicable: opts.includeNotApplicable,
includeEvidence: opts.includeEvidence,
includeGapAnalysis: opts.includeGapAnalysis,
}
const docxContent = generateDOCXContent(state, docxOptions)
files.push({
path: 'documents/tom-document-structure.json',
content: JSON.stringify(docxContent, null, 2),
mimeType: 'application/json',
})
}
// PDF content structure (if requested)
if (opts.formats.includes('pdf')) {
const pdfOptions: Partial<PDFExportOptions> = {
language: opts.language,
includeNotApplicable: opts.includeNotApplicable,
includeEvidence: opts.includeEvidence,
includeGapAnalysis: opts.includeGapAnalysis,
}
const pdfContent = generatePDFContent(state, pdfOptions)
files.push({
path: 'documents/tom-document-structure-pdf.json',
content: JSON.stringify(pdfContent, null, 2),
mimeType: 'application/json',
})
}
// Markdown summary
files.push({
path: 'documents/tom-summary.md',
content: generateMarkdownSummary(state, opts),
mimeType: 'text/markdown',
})
// CSV export for spreadsheet import
files.push({
path: 'documents/toms.csv',
content: generateCSV(state.derivedTOMs, opts),
mimeType: 'text/csv',
})
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
// =============================================================================
/**
* Generate a ZIP file as a Blob
* This is a placeholder - in production, use jszip library
*/
export async function generateZIPBlob(
state: TOMGeneratorState,
options: Partial<ZIPExportOptions> = {}
): Promise<Blob> {
const files = generateZIPFiles(state, options)
// Create a simple JSON representation for now
// In production, use JSZip library
const manifest = {
generated: new Date().toISOString(),
files: files.map((f) => ({
path: f.path,
mimeType: f.mimeType,
size: typeof f.content === 'string' ? f.content.length : 0,
})),
}
const allContent = files
.filter((f) => typeof f.content === 'string')
.map((f) => `\n\n=== ${f.path} ===\n\n${f.content}`)
.join('\n')
const output = `TOM Export Package
Generated: ${manifest.generated}
Files:
${manifest.files.map((f) => ` - ${f.path} (${f.mimeType})`).join('\n')}
${allContent}`
return new Blob([output], { type: 'application/zip' })
}
// =============================================================================
// FILENAME GENERATION
// =============================================================================
/**
* Generate a filename for ZIP export
*/
export function generateZIPFilename(
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-Export' : 'TOMs-Export'
return `${prefix}-${companyName}-${date}.zip`
}
// =============================================================================
// EXPORT
// =============================================================================
// Types are exported at their definition site above