// ============================================================================= // Loeschfristen Module - Export & Report Generation // JSON, CSV, Markdown-Compliance-Report und Browser-Download // ============================================================================= import { LoeschfristPolicy, RETENTION_DRIVER_META, DELETION_METHOD_LABELS, STATUS_LABELS, TRIGGER_LABELS, formatRetentionDuration, getEffectiveDeletionTrigger, } from './loeschfristen-types' import { runComplianceCheck, ComplianceCheckResult, ComplianceIssueSeverity, } from './loeschfristen-compliance' // ============================================================================= // JSON EXPORT // ============================================================================= interface PolicyExportEnvelope { exportDate: string version: string totalPolicies: number policies: LoeschfristPolicy[] } /** * Exportiert alle Policies als pretty-printed JSON. * Enthaelt Metadaten (Exportdatum, Version, Anzahl). */ export function exportPoliciesAsJSON(policies: LoeschfristPolicy[]): string { const exportData: PolicyExportEnvelope = { exportDate: new Date().toISOString(), version: '1.0', totalPolicies: policies.length, policies: policies, } return JSON.stringify(exportData, null, 2) } // ============================================================================= // CSV EXPORT // ============================================================================= /** * Escapes a CSV field value according to RFC 4180. * Fields containing commas, double quotes, or newlines are wrapped in quotes. * Existing double quotes are doubled. */ function escapeCSVField(value: string): string { if ( value.includes(',') || value.includes('"') || value.includes('\n') || value.includes('\r') || value.includes(';') ) { return `"${value.replace(/"/g, '""')}"` } return value } /** * Formats a date string to German locale format (DD.MM.YYYY). * Returns empty string for null/undefined/empty values. */ function formatDateDE(dateStr: string | null | undefined): string { if (!dateStr) return '' try { const date = new Date(dateStr) if (isNaN(date.getTime())) return '' return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }) } catch { return '' } } /** * Exportiert alle Policies als CSV mit BOM fuer Excel-Kompatibilitaet. * Trennzeichen ist Semikolon (;) fuer deutschsprachige Excel-Versionen. */ export function exportPoliciesAsCSV(policies: LoeschfristPolicy[]): string { const BOM = '\uFEFF' const SEPARATOR = ';' const headers = [ 'LF-Nr.', 'Datenobjekt', 'Beschreibung', 'Loeschtrigger', 'Aufbewahrungstreiber', 'Frist', 'Startereignis', 'Loeschmethode', 'Verantwortlich', 'Status', 'Legal Hold aktiv', 'Letzte Pruefung', 'Naechste Pruefung', ] const rows: string[] = [] // Header row rows.push(headers.map(escapeCSVField).join(SEPARATOR)) // Data rows for (const policy of policies) { const effectiveTrigger = getEffectiveDeletionTrigger(policy) const triggerLabel = TRIGGER_LABELS[effectiveTrigger] const driverLabel = policy.retentionDriver ? RETENTION_DRIVER_META[policy.retentionDriver].label : '' const durationLabel = formatRetentionDuration( policy.retentionDuration, policy.retentionUnit ) const methodLabel = DELETION_METHOD_LABELS[policy.deletionMethod] const statusLabel = STATUS_LABELS[policy.status] // Combine responsiblePerson and responsibleRole const responsible = [policy.responsiblePerson, policy.responsibleRole] .filter((s) => s.trim()) .join(' / ') const legalHoldActive = policy.hasActiveLegalHold ? 'Ja' : 'Nein' const row = [ policy.policyId, policy.dataObjectName, policy.description, triggerLabel, driverLabel, durationLabel, policy.startEvent, methodLabel, responsible || '-', statusLabel, legalHoldActive, formatDateDE(policy.lastReviewDate), formatDateDE(policy.nextReviewDate), ] rows.push(row.map(escapeCSVField).join(SEPARATOR)) } return BOM + rows.join('\r\n') } // ============================================================================= // COMPLIANCE SUMMARY (MARKDOWN) // ============================================================================= const SEVERITY_LABELS: Record = { CRITICAL: 'Kritisch', HIGH: 'Hoch', MEDIUM: 'Mittel', LOW: 'Niedrig', } const SEVERITY_EMOJI: Record = { CRITICAL: '[!!!]', HIGH: '[!!]', MEDIUM: '[!]', LOW: '[i]', } /** * Returns a textual rating based on the compliance score. */ function getScoreRating(score: number): string { if (score >= 90) return 'Ausgezeichnet' if (score >= 75) return 'Gut' if (score >= 50) return 'Verbesserungswuerdig' if (score >= 25) return 'Mangelhaft' return 'Kritisch' } /** * Generiert einen Markdown-formatierten Compliance-Bericht. * Enthaelt: Uebersicht, Score, Issue-Liste, Empfehlungen. */ export function generateComplianceSummary( policies: LoeschfristPolicy[], vvtDataCategories?: string[] ): string { const result: ComplianceCheckResult = runComplianceCheck(policies, vvtDataCategories) const now = new Date() const lines: string[] = [] // Header lines.push('# Compliance-Bericht: Loeschfristen') lines.push('') lines.push( `**Erstellt am:** ${now.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} um ${now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} Uhr` ) lines.push('') // Overview lines.push('## Uebersicht') lines.push('') lines.push(`| Kennzahl | Wert |`) lines.push(`|----------|------|`) lines.push(`| Gepruefte Policies | ${result.stats.total} |`) lines.push(`| Bestanden | ${result.stats.passed} |`) lines.push(`| Beanstandungen | ${result.stats.failed} |`) lines.push(`| Compliance-Score | **${result.score}/100** (${getScoreRating(result.score)}) |`) lines.push('') // Severity breakdown lines.push('## Befunde nach Schweregrad') lines.push('') lines.push('| Schweregrad | Anzahl |') lines.push('|-------------|--------|') const severityOrder: ComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] for (const severity of severityOrder) { const count = result.stats.bySeverity[severity] lines.push(`| ${SEVERITY_LABELS[severity]} | ${count} |`) } lines.push('') // Status distribution of policies const statusCounts: Record = {} for (const policy of policies) { const label = STATUS_LABELS[policy.status] statusCounts[label] = (statusCounts[label] || 0) + 1 } lines.push('## Policy-Status-Verteilung') lines.push('') lines.push('| Status | Anzahl |') lines.push('|--------|--------|') for (const [label, count] of Object.entries(statusCounts)) { lines.push(`| ${label} | ${count} |`) } lines.push('') // Issues list if (result.issues.length === 0) { lines.push('## Befunde') lines.push('') lines.push('Keine Beanstandungen gefunden. Alle Policies sind konform.') lines.push('') } else { lines.push('## Befunde') lines.push('') // Group issues by severity for (const severity of severityOrder) { const issuesForSeverity = result.issues.filter((i) => i.severity === severity) if (issuesForSeverity.length === 0) continue lines.push(`### ${SEVERITY_LABELS[severity]} ${SEVERITY_EMOJI[severity]}`) lines.push('') for (const issue of issuesForSeverity) { const policyRef = issue.policyId !== '-' ? ` (${issue.policyId})` : '' lines.push(`**${issue.title}**${policyRef}`) lines.push('') lines.push(`> ${issue.description}`) lines.push('') lines.push(`Empfehlung: ${issue.recommendation}`) lines.push('') lines.push('---') lines.push('') } } } // Recommendations summary lines.push('## Zusammenfassung der Empfehlungen') lines.push('') if (result.stats.bySeverity.CRITICAL > 0) { lines.push( `1. **Sofortmassnahmen erforderlich:** ${result.stats.bySeverity.CRITICAL} kritische(r) Befund(e) muessen umgehend behoben werden (Legal Hold-Konflikte).` ) } if (result.stats.bySeverity.HIGH > 0) { lines.push( `${result.stats.bySeverity.CRITICAL > 0 ? '2' : '1'}. **Hohe Prioritaet:** ${result.stats.bySeverity.HIGH} Befund(e) mit hoher Prioritaet (fehlende Trigger/Rechtsgrundlagen) sollten zeitnah bearbeitet werden.` ) } if (result.stats.bySeverity.MEDIUM > 0) { lines.push( `- **Mittlere Prioritaet:** ${result.stats.bySeverity.MEDIUM} Befund(e) betreffen ueberfaellige Pruefungen, fehlende Verantwortlichkeiten oder nicht abgedeckte Datenkategorien.` ) } if (result.stats.bySeverity.LOW > 0) { lines.push( `- **Niedrige Prioritaet:** ${result.stats.bySeverity.LOW} Befund(e) betreffen veraltete Entwuerfe, die finalisiert oder archiviert werden sollten.` ) } if (result.issues.length === 0) { lines.push( 'Alle Policies sind konform. Stellen Sie sicher, dass die naechsten Pruefungstermine eingehalten werden.' ) } lines.push('') // Footer lines.push('---') lines.push('') lines.push( '*Dieser Bericht wurde automatisch generiert und ersetzt keine rechtliche Beratung. Die Verantwortung fuer die DSGVO-Konformitaet liegt beim Verantwortlichen (Art. 4 Nr. 7 DSGVO).*' ) return lines.join('\n') } // ============================================================================= // BROWSER DOWNLOAD UTILITY // ============================================================================= /** * Loest einen Datei-Download im Browser aus. * Erstellt ein temporaeres Blob-URL und simuliert einen Link-Klick. * * @param content - Der Dateiinhalt als String * @param filename - Der gewuenschte Dateiname (z.B. "loeschfristen-export.json") * @param mimeType - Der MIME-Typ (z.B. "application/json", "text/csv;charset=utf-8") */ export function downloadFile( content: string, filename: string, mimeType: string ): void { const blob = new Blob([content], { type: mimeType }) 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) }