/** * SDK Export Utilities * Handles PDF and ZIP export of SDK state and documents */ import jsPDF from 'jspdf' import JSZip from 'jszip' import { SDKState, SDK_STEPS, getStepById } from './types' // ============================================================================= // TYPES // ============================================================================= export interface ExportOptions { includeEvidence?: boolean includeDocuments?: boolean includeRawData?: boolean language?: 'de' | 'en' } const DEFAULT_OPTIONS: ExportOptions = { includeEvidence: true, includeDocuments: true, includeRawData: true, language: 'de', } // ============================================================================= // LABELS (German) // ============================================================================= const LABELS_DE = { title: 'AI Compliance SDK - Export', subtitle: 'Compliance-Dokumentation', generatedAt: 'Generiert am', page: 'Seite', summary: 'Zusammenfassung', progress: 'Fortschritt', phase1: 'Phase 1: Automatisches Compliance Assessment', phase2: 'Phase 2: Dokumentengenerierung', useCases: 'Use Cases', risks: 'Risiken', controls: 'Controls', requirements: 'Anforderungen', modules: 'Compliance-Module', evidence: 'Nachweise', checkpoints: 'Checkpoints', noData: 'Keine Daten vorhanden', status: 'Status', completed: 'Abgeschlossen', pending: 'Ausstehend', inProgress: 'In Bearbeitung', severity: 'Schweregrad', mitigation: 'Mitigation', description: 'Beschreibung', category: 'Kategorie', implementation: 'Implementierung', } // ============================================================================= // PDF EXPORT // ============================================================================= function formatDate(date: Date | string | undefined): string { if (!date) return '-' const d = typeof date === 'string' ? new Date(date) : date return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }) } function addHeader(doc: jsPDF, title: string, pageNum: number, totalPages: number): void { const pageWidth = doc.internal.pageSize.getWidth() // Header line doc.setDrawColor(147, 51, 234) // Purple doc.setLineWidth(0.5) doc.line(20, 15, pageWidth - 20, 15) // Title doc.setFontSize(10) doc.setTextColor(100) doc.text(title, 20, 12) // Page number doc.text(`${LABELS_DE.page} ${pageNum}/${totalPages}`, pageWidth - 40, 12) } function addFooter(doc: jsPDF, state: SDKState): void { const pageWidth = doc.internal.pageSize.getWidth() const pageHeight = doc.internal.pageSize.getHeight() // Footer line doc.setDrawColor(200) doc.setLineWidth(0.3) doc.line(20, pageHeight - 15, pageWidth - 20, pageHeight - 15) // Footer text doc.setFontSize(8) doc.setTextColor(150) doc.text(`Tenant: ${state.tenantId} | ${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 20, pageHeight - 10) } function addSectionTitle(doc: jsPDF, title: string, y: number): number { doc.setFontSize(14) doc.setTextColor(147, 51, 234) // Purple doc.setFont('helvetica', 'bold') doc.text(title, 20, y) doc.setFont('helvetica', 'normal') return y + 10 } function addSubsectionTitle(doc: jsPDF, title: string, y: number): number { doc.setFontSize(11) doc.setTextColor(60) doc.setFont('helvetica', 'bold') doc.text(title, 25, y) doc.setFont('helvetica', 'normal') return y + 7 } function addText(doc: jsPDF, text: string, x: number, y: number, maxWidth: number = 170): number { doc.setFontSize(10) doc.setTextColor(60) const lines = doc.splitTextToSize(text, maxWidth) doc.text(lines, x, y) return y + lines.length * 5 } function checkPageBreak(doc: jsPDF, y: number, requiredSpace: number = 40): number { const pageHeight = doc.internal.pageSize.getHeight() if (y + requiredSpace > pageHeight - 25) { doc.addPage() return 30 } return y } export async function exportToPDF(state: SDKState, options: ExportOptions = {}): Promise { const opts = { ...DEFAULT_OPTIONS, ...options } const doc = new jsPDF() let y = 30 const pageWidth = doc.internal.pageSize.getWidth() // ========================================================================== // Title Page // ========================================================================== // Logo/Title area doc.setFillColor(147, 51, 234) doc.rect(0, 0, pageWidth, 60, 'F') doc.setFontSize(24) doc.setTextColor(255) doc.setFont('helvetica', 'bold') doc.text(LABELS_DE.title, 20, 35) doc.setFontSize(14) doc.setFont('helvetica', 'normal') doc.text(LABELS_DE.subtitle, 20, 48) // Reset for content y = 80 // Summary box doc.setDrawColor(200) doc.setFillColor(249, 250, 251) doc.roundedRect(20, y, pageWidth - 40, 50, 3, 3, 'FD') y += 15 doc.setFontSize(12) doc.setTextColor(60) doc.text(`${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 30, y) y += 10 doc.text(`Tenant ID: ${state.tenantId}`, 30, y) y += 10 doc.text(`Version: ${state.version}`, 30, y) y += 10 const completedSteps = state.completedSteps.length const totalSteps = SDK_STEPS.length doc.text(`${LABELS_DE.progress}: ${completedSteps}/${totalSteps} Schritte (${Math.round(completedSteps / totalSteps * 100)}%)`, 30, y) y += 30 // Table of Contents y = addSectionTitle(doc, 'Inhaltsverzeichnis', y) const tocItems = [ { title: 'Zusammenfassung', page: 2 }, { title: 'Phase 1: Compliance Assessment', page: 3 }, { title: 'Phase 2: Dokumentengenerierung', page: 4 }, { title: 'Risiken & Controls', page: 5 }, { title: 'Checkpoints', page: 6 }, ] doc.setFontSize(10) doc.setTextColor(80) tocItems.forEach((item, idx) => { doc.text(`${idx + 1}. ${item.title}`, 25, y) doc.text(`${item.page}`, pageWidth - 30, y, { align: 'right' }) y += 7 }) // ========================================================================== // Summary Page // ========================================================================== doc.addPage() y = 30 y = addSectionTitle(doc, LABELS_DE.summary, y) // Progress overview doc.setFillColor(249, 250, 251) doc.roundedRect(20, y, pageWidth - 40, 40, 3, 3, 'F') y += 15 const phase1Steps = SDK_STEPS.filter(s => s.phase === 1) const phase2Steps = SDK_STEPS.filter(s => s.phase === 2) const phase1Completed = phase1Steps.filter(s => state.completedSteps.includes(s.id)).length const phase2Completed = phase2Steps.filter(s => state.completedSteps.includes(s.id)).length doc.setFontSize(10) doc.setTextColor(60) doc.text(`${LABELS_DE.phase1}: ${phase1Completed}/${phase1Steps.length} ${LABELS_DE.completed}`, 30, y) y += 8 doc.text(`${LABELS_DE.phase2}: ${phase2Completed}/${phase2Steps.length} ${LABELS_DE.completed}`, 30, y) y += 25 // Key metrics y = addSubsectionTitle(doc, 'Kennzahlen', y) const metrics = [ { label: 'Use Cases', value: state.useCases.length }, { label: 'Risiken identifiziert', value: state.risks.length }, { label: 'Controls definiert', value: state.controls.length }, { label: 'Anforderungen', value: state.requirements.length }, { label: 'Nachweise', value: state.evidence.length }, ] metrics.forEach(metric => { doc.text(`${metric.label}: ${metric.value}`, 30, y) y += 7 }) // ========================================================================== // Use Cases // ========================================================================== y += 10 y = checkPageBreak(doc, y) y = addSectionTitle(doc, LABELS_DE.useCases, y) if (state.useCases.length === 0) { y = addText(doc, LABELS_DE.noData, 25, y) } else { state.useCases.forEach((uc, idx) => { y = checkPageBreak(doc, y, 50) doc.setFillColor(249, 250, 251) doc.roundedRect(20, y - 5, pageWidth - 40, 35, 2, 2, 'F') doc.setFontSize(11) doc.setTextColor(40) doc.setFont('helvetica', 'bold') doc.text(`${idx + 1}. ${uc.name}`, 25, y + 5) doc.setFont('helvetica', 'normal') doc.setFontSize(9) doc.setTextColor(100) const ucStatus = uc.stepsCompleted === uc.steps.length ? LABELS_DE.completed : `${uc.stepsCompleted}/${uc.steps.length} Schritte` doc.text(`ID: ${uc.id} | ${LABELS_DE.status}: ${ucStatus}`, 25, y + 13) if (uc.description) { y = addText(doc, uc.description, 25, y + 21, 160) } y += 40 }) } // ========================================================================== // Risks // ========================================================================== doc.addPage() y = 30 y = addSectionTitle(doc, LABELS_DE.risks, y) if (state.risks.length === 0) { y = addText(doc, LABELS_DE.noData, 25, y) } else { // Sort by severity const sortedRisks = [...state.risks].sort((a, b) => { const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 } return (order[a.severity] || 4) - (order[b.severity] || 4) }) sortedRisks.forEach((risk, idx) => { y = checkPageBreak(doc, y, 45) // Severity color const severityColors: Record = { CRITICAL: [220, 38, 38], HIGH: [234, 88, 12], MEDIUM: [234, 179, 8], LOW: [34, 197, 94], } const color = severityColors[risk.severity] || [100, 100, 100] doc.setFillColor(color[0], color[1], color[2]) doc.rect(20, y - 3, 3, 30, 'F') doc.setFontSize(11) doc.setTextColor(40) doc.setFont('helvetica', 'bold') doc.text(`${idx + 1}. ${risk.title}`, 28, y + 5) doc.setFont('helvetica', 'normal') doc.setFontSize(9) doc.setTextColor(100) doc.text(`${LABELS_DE.severity}: ${risk.severity} | ${LABELS_DE.category}: ${risk.category}`, 28, y + 13) if (risk.description) { y = addText(doc, risk.description, 28, y + 21, 155) } if (risk.mitigation && risk.mitigation.length > 0) { y += 5 doc.setFontSize(9) doc.setTextColor(34, 197, 94) doc.text(`${LABELS_DE.mitigation}: ${risk.mitigation.join(', ')}`, 28, y) } y += 15 }) } // ========================================================================== // Controls // ========================================================================== doc.addPage() y = 30 y = addSectionTitle(doc, LABELS_DE.controls, y) if (state.controls.length === 0) { y = addText(doc, LABELS_DE.noData, 25, y) } else { state.controls.forEach((ctrl, idx) => { y = checkPageBreak(doc, y, 35) doc.setFillColor(249, 250, 251) doc.roundedRect(20, y - 5, pageWidth - 40, 28, 2, 2, 'F') doc.setFontSize(10) doc.setTextColor(40) doc.setFont('helvetica', 'bold') doc.text(`${idx + 1}. ${ctrl.name}`, 25, y + 5) doc.setFont('helvetica', 'normal') doc.setFontSize(9) doc.setTextColor(100) doc.text(`${LABELS_DE.category}: ${ctrl.category} | ${LABELS_DE.implementation}: ${ctrl.implementationStatus || 'Nicht definiert'}`, 25, y + 13) if (ctrl.description) { y = addText(doc, ctrl.description.substring(0, 150) + (ctrl.description.length > 150 ? '...' : ''), 25, y + 20, 160) } y += 35 }) } // ========================================================================== // Checkpoints // ========================================================================== doc.addPage() y = 30 y = addSectionTitle(doc, LABELS_DE.checkpoints, y) const checkpointIds = Object.keys(state.checkpoints) if (checkpointIds.length === 0) { y = addText(doc, LABELS_DE.noData, 25, y) } else { checkpointIds.forEach((cpId) => { const cp = state.checkpoints[cpId] y = checkPageBreak(doc, y, 25) const statusColor = cp.passed ? [34, 197, 94] : [220, 38, 38] doc.setFillColor(statusColor[0], statusColor[1], statusColor[2]) doc.circle(25, y + 2, 3, 'F') doc.setFontSize(10) doc.setTextColor(40) doc.text(cpId, 35, y + 5) doc.setFontSize(9) doc.setTextColor(100) doc.text(`${LABELS_DE.status}: ${cp.passed ? LABELS_DE.completed : LABELS_DE.pending}`, 35, y + 12) if (cp.errors && cp.errors.length > 0) { doc.setTextColor(220, 38, 38) doc.text(`Fehler: ${cp.errors.map(e => e.message).join(', ')}`, 35, y + 19) y += 7 } y += 20 }) } // ========================================================================== // Add page numbers // ========================================================================== const pageCount = doc.getNumberOfPages() for (let i = 1; i <= pageCount; i++) { doc.setPage(i) if (i > 1) { addHeader(doc, LABELS_DE.title, i, pageCount) } addFooter(doc, state) } return doc.output('blob') } // ============================================================================= // ZIP EXPORT // ============================================================================= export async function exportToZIP(state: SDKState, options: ExportOptions = {}): Promise { const opts = { ...DEFAULT_OPTIONS, ...options } const zip = new JSZip() // Create folder structure const rootFolder = zip.folder('ai-compliance-sdk-export') if (!rootFolder) throw new Error('Failed to create ZIP folder') const phase1Folder = rootFolder.folder('phase1-assessment') const phase2Folder = rootFolder.folder('phase2-documents') const dataFolder = rootFolder.folder('data') // ========================================================================== // Main State JSON // ========================================================================== if (opts.includeRawData && dataFolder) { dataFolder.file('state.json', JSON.stringify(state, null, 2)) } // ========================================================================== // README // ========================================================================== const readmeContent = `# AI Compliance SDK Export Generated: ${formatDate(new Date())} Tenant: ${state.tenantId} Version: ${state.version} ## Folder Structure - **phase1-assessment/**: Compliance Assessment Ergebnisse - use-cases.json: Alle Use Cases - risks.json: Identifizierte Risiken - controls.json: Definierte Controls - requirements.json: Compliance-Anforderungen - **phase2-documents/**: Generierte Dokumente - dsfa.json: Datenschutz-Folgenabschaetzung - toms.json: Technische und organisatorische Massnahmen - vvt.json: Verarbeitungsverzeichnis - documents.json: Rechtliche Dokumente - **data/**: Rohdaten - state.json: Kompletter SDK State ## Progress Phase 1: ${SDK_STEPS.filter(s => s.phase === 1 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 1).length} completed Phase 2: ${SDK_STEPS.filter(s => s.phase === 2 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 2).length} completed ## Key Metrics - Use Cases: ${state.useCases.length} - Risks: ${state.risks.length} - Controls: ${state.controls.length} - Requirements: ${state.requirements.length} - Evidence: ${state.evidence.length} ` rootFolder.file('README.md', readmeContent) // ========================================================================== // Phase 1 Files // ========================================================================== if (phase1Folder) { // Use Cases phase1Folder.file('use-cases.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.useCases.length, useCases: state.useCases, }, null, 2)) // Risks phase1Folder.file('risks.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.risks.length, risks: state.risks, summary: { critical: state.risks.filter(r => r.severity === 'CRITICAL').length, high: state.risks.filter(r => r.severity === 'HIGH').length, medium: state.risks.filter(r => r.severity === 'MEDIUM').length, low: state.risks.filter(r => r.severity === 'LOW').length, }, }, null, 2)) // Controls phase1Folder.file('controls.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.controls.length, controls: state.controls, }, null, 2)) // Requirements phase1Folder.file('requirements.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.requirements.length, requirements: state.requirements, }, null, 2)) // Modules phase1Folder.file('modules.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.modules.length, modules: state.modules, }, null, 2)) // Evidence if (opts.includeEvidence) { phase1Folder.file('evidence.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.evidence.length, evidence: state.evidence, }, null, 2)) } // Checkpoints phase1Folder.file('checkpoints.json', JSON.stringify({ exportedAt: new Date().toISOString(), checkpoints: state.checkpoints, }, null, 2)) // Screening if (state.screening) { phase1Folder.file('screening.json', JSON.stringify({ exportedAt: new Date().toISOString(), screening: state.screening, }, null, 2)) } } // ========================================================================== // Phase 2 Files // ========================================================================== if (phase2Folder) { // DSFA if (state.dsfa) { phase2Folder.file('dsfa.json', JSON.stringify({ exportedAt: new Date().toISOString(), dsfa: state.dsfa, }, null, 2)) } // TOMs phase2Folder.file('toms.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.toms.length, toms: state.toms, }, null, 2)) // VVT (Processing Activities) phase2Folder.file('vvt.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.vvt.length, processingActivities: state.vvt, }, null, 2)) // Legal Documents if (opts.includeDocuments) { phase2Folder.file('documents.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.documents.length, documents: state.documents, }, null, 2)) } // Cookie Banner Config if (state.cookieBanner) { phase2Folder.file('cookie-banner.json', JSON.stringify({ exportedAt: new Date().toISOString(), config: state.cookieBanner, }, null, 2)) } // Retention Policies phase2Folder.file('retention-policies.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.retentionPolicies.length, policies: state.retentionPolicies, }, null, 2)) // AI Act Classification if (state.aiActClassification) { phase2Folder.file('ai-act-classification.json', JSON.stringify({ exportedAt: new Date().toISOString(), classification: state.aiActClassification, }, null, 2)) } // Obligations phase2Folder.file('obligations.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.obligations.length, obligations: state.obligations, }, null, 2)) // Consent Records phase2Folder.file('consents.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.consents.length, consents: state.consents, }, null, 2)) // DSR Config if (state.dsrConfig) { phase2Folder.file('dsr-config.json', JSON.stringify({ exportedAt: new Date().toISOString(), config: state.dsrConfig, }, null, 2)) } // Escalation Workflows phase2Folder.file('escalation-workflows.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.escalationWorkflows.length, workflows: state.escalationWorkflows, }, null, 2)) } // ========================================================================== // Security Data // ========================================================================== if (dataFolder) { if (state.sbom) { dataFolder.file('sbom.json', JSON.stringify({ exportedAt: new Date().toISOString(), sbom: state.sbom, }, null, 2)) } if (state.securityIssues.length > 0) { dataFolder.file('security-issues.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.securityIssues.length, issues: state.securityIssues, }, null, 2)) } if (state.securityBacklog.length > 0) { dataFolder.file('security-backlog.json', JSON.stringify({ exportedAt: new Date().toISOString(), count: state.securityBacklog.length, backlog: state.securityBacklog, }, null, 2)) } } // ========================================================================== // Generate PDF and include in ZIP // ========================================================================== try { const pdfBlob = await exportToPDF(state, options) const pdfArrayBuffer = await pdfBlob.arrayBuffer() rootFolder.file('compliance-report.pdf', pdfArrayBuffer) } catch (error) { console.error('Failed to generate PDF for ZIP:', error) // Continue without PDF } // Generate ZIP return zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }) } // ============================================================================= // EXPORT HELPER // ============================================================================= export async function downloadExport( state: SDKState, format: 'json' | 'pdf' | 'zip', options: ExportOptions = {} ): Promise { let blob: Blob let filename: string const timestamp = new Date().toISOString().slice(0, 10) switch (format) { case 'json': blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' }) filename = `ai-compliance-sdk-${timestamp}.json` break case 'pdf': blob = await exportToPDF(state, options) filename = `ai-compliance-sdk-${timestamp}.pdf` break case 'zip': blob = await exportToZIP(state, options) filename = `ai-compliance-sdk-${timestamp}.zip` break default: throw new Error(`Unknown export format: ${format}`) } // Create download link const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = filename document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) }