feat(admin-v2): Major SDK/Compliance overhaul and new modules
SDK modules added/enhanced: - compliance-hub, compliance-scope, consent-management, notfallplan - audit-report, workflow, source-policy, dsms - advisory-board documentation section - TOM dashboard components, TOM generator SDM mapping - DSFA: mitigation library, risk catalog, threshold analysis, source attribution - VVT: baseline catalog, profiling engine, types - Loeschfristen: baseline catalog, compliance engine, export, profiling, types - Compliance scope: engine, profiling, golden tests, types Existing SDK pages updated: - dsfa/[id], tom, vvt, loeschfristen, advisory-board — expanded functionality - SDKSidebar, StepHeader — new navigation items and layout - SDK layout, context, types — expanded type system Other admin-v2 changes: - AI agents page, RAG pipeline DSFA integration - GridOverlay component updates - Companion feature (development + education) - Compliance advisor SOUL definition Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
353
admin-v2/lib/sdk/loeschfristen-export.ts
Normal file
353
admin-v2/lib/sdk/loeschfristen-export.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
// =============================================================================
|
||||
// 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<ComplianceIssueSeverity, string> = {
|
||||
CRITICAL: 'Kritisch',
|
||||
HIGH: 'Hoch',
|
||||
MEDIUM: 'Mittel',
|
||||
LOW: 'Niedrig',
|
||||
}
|
||||
|
||||
const SEVERITY_EMOJI: Record<ComplianceIssueSeverity, string> = {
|
||||
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<string, number> = {}
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user