// ============================================================================= // Loeschfristen Module - Loeschkonzept Document Generator // Generates a printable, audit-ready HTML document according to DSGVO Art. 5/17/30 // ============================================================================= import type { LoeschfristPolicy, RetentionDriverType, } from './loeschfristen-types' import { RETENTION_DRIVER_META, DELETION_METHOD_LABELS, STATUS_LABELS, TRIGGER_LABELS, REVIEW_INTERVAL_LABELS, formatRetentionDuration, getEffectiveDeletionTrigger, getActiveLegalHolds, } from './loeschfristen-types' import type { ComplianceCheckResult, ComplianceIssueSeverity } from './loeschfristen-compliance' // ============================================================================= // TYPES // ============================================================================= export interface LoeschkonzeptOrgHeader { organizationName: string industry: string dpoName: string dpoContact: string responsiblePerson: string locations: string[] employeeCount: string loeschkonzeptVersion: string lastReviewDate: string nextReviewDate: string reviewInterval: string } export interface LoeschkonzeptRevision { version: string date: string author: string changes: string } // ============================================================================= // DEFAULTS // ============================================================================= export function createDefaultLoeschkonzeptOrgHeader(): LoeschkonzeptOrgHeader { const now = new Date() const nextYear = new Date() nextYear.setFullYear(nextYear.getFullYear() + 1) return { organizationName: '', industry: '', dpoName: '', dpoContact: '', responsiblePerson: '', locations: [], employeeCount: '', loeschkonzeptVersion: '1.0', lastReviewDate: now.toISOString().split('T')[0], nextReviewDate: nextYear.toISOString().split('T')[0], reviewInterval: 'Jaehrlich', } } // ============================================================================= // SEVERITY LABELS (for Compliance Status section) // ============================================================================= const SEVERITY_LABELS_DE: Record = { CRITICAL: 'Kritisch', HIGH: 'Hoch', MEDIUM: 'Mittel', LOW: 'Niedrig', } const SEVERITY_COLORS: Record = { CRITICAL: '#dc2626', HIGH: '#ea580c', MEDIUM: '#d97706', LOW: '#6b7280', } // ============================================================================= // HTML DOCUMENT BUILDER // ============================================================================= export function buildLoeschkonzeptHtml( policies: LoeschfristPolicy[], orgHeader: LoeschkonzeptOrgHeader, vvtActivities: Array<{ id: string; vvt_id?: string; vvtId?: string; name?: string; activity_name?: string }>, complianceResult: ComplianceCheckResult | null, revisions: LoeschkonzeptRevision[] ): string { const today = new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }) const activePolicies = policies.filter(p => p.status !== 'ARCHIVED') const orgName = orgHeader.organizationName || 'Organisation' // Collect unique storage locations across all policies const allStorageLocations = new Set() for (const p of activePolicies) { for (const loc of p.storageLocations) { allStorageLocations.add(loc.name || loc.type) } } // Collect unique responsible roles const roleMap = new Map() for (const p of activePolicies) { const role = p.responsibleRole || p.responsiblePerson || 'Nicht zugewiesen' if (!roleMap.has(role)) roleMap.set(role, []) roleMap.get(role)!.push(p.dataObjectName || p.policyId) } // Collect active legal holds const allActiveLegalHolds: Array<{ policy: string; hold: LoeschfristPolicy['legalHolds'][0] }> = [] for (const p of activePolicies) { for (const h of getActiveLegalHolds(p)) { allActiveLegalHolds.push({ policy: p.dataObjectName || p.policyId, hold: h }) } } // Build VVT cross-reference data const vvtRefs: Array<{ policyName: string; policyId: string; vvtId: string; vvtName: string }> = [] for (const p of activePolicies) { for (const linkedId of p.linkedVVTActivityIds) { const activity = vvtActivities.find(a => a.id === linkedId) if (activity) { vvtRefs.push({ policyName: p.dataObjectName || p.policyId, policyId: p.policyId, vvtId: activity.vvt_id || activity.vvtId || linkedId.substring(0, 8), vvtName: activity.activity_name || activity.name || 'Unbenannte Verarbeitungstaetigkeit', }) } } } // Build vendor cross-reference data const vendorRefs: Array<{ policyName: string; policyId: string; vendorId: string; duration: string }> = [] for (const p of activePolicies) { if (p.linkedVendorIds && p.linkedVendorIds.length > 0) { for (const vendorId of p.linkedVendorIds) { vendorRefs.push({ policyName: p.dataObjectName || p.policyId, policyId: p.policyId, vendorId, duration: formatRetentionDuration(p.retentionDuration, p.retentionUnit), }) } } } // ========================================================================= // HTML Template // ========================================================================= let html = ` Loeschkonzept — ${escHtml(orgName)} ` // ========================================================================= // Section 0: Cover Page // ========================================================================= html += `

Loeschkonzept

gemaess Art. 5 Abs. 1 lit. e, Art. 17, Art. 30 DSGVO
Organisation: ${escHtml(orgName)}
${orgHeader.industry ? `
Branche: ${escHtml(orgHeader.industry)}
` : ''} ${orgHeader.dpoName ? `
Datenschutzbeauftragter: ${escHtml(orgHeader.dpoName)}
` : ''} ${orgHeader.dpoContact ? `
DSB-Kontakt: ${escHtml(orgHeader.dpoContact)}
` : ''} ${orgHeader.responsiblePerson ? `
Verantwortlicher: ${escHtml(orgHeader.responsiblePerson)}
` : ''} ${orgHeader.employeeCount ? `
Mitarbeiter: ${escHtml(orgHeader.employeeCount)}
` : ''} ${orgHeader.locations.length > 0 ? `
Standorte: ${escHtml(orgHeader.locations.join(', '))}
` : ''}
` // ========================================================================= // Table of Contents // ========================================================================= const sections = [ 'Ziel und Zweck', 'Geltungsbereich', 'Grundprinzipien der Datenspeicherung', 'Loeschregeln-Uebersicht', 'Detaillierte Loeschregeln', 'VVT-Verknuepfung', 'Auftragsverarbeiter mit Loeschpflichten', 'Legal Hold Verfahren', 'Verantwortlichkeiten', 'Pruef- und Revisionszyklus', 'Compliance-Status', 'Aenderungshistorie', ] html += `

Inhaltsverzeichnis

${sections.map((s, i) => `
${i + 1}. ${escHtml(s)}
`).join('\n ')}
` // ========================================================================= // Section 1: Ziel und Zweck // ========================================================================= html += `
1. Ziel und Zweck

Dieses Loeschkonzept definiert die systematischen Regeln und Verfahren fuer die Loeschung personenbezogener Daten bei ${escHtml(orgName)}. Es dient der Umsetzung folgender DSGVO-Anforderungen:

RechtsgrundlageInhalt
Art. 5 Abs. 1 lit. e DSGVOGrundsatz der Speicherbegrenzung — personenbezogene Daten duerfen nur so lange gespeichert werden, wie es fuer die Zwecke der Verarbeitung erforderlich ist.
Art. 17 DSGVORecht auf Loeschung („Recht auf Vergessenwerden“) — Betroffene haben das Recht, die Loeschung ihrer Daten zu verlangen.
Art. 30 DSGVOVerzeichnis von Verarbeitungstaetigkeiten — vorgesehene Fristen fuer die Loeschung der verschiedenen Datenkategorien muessen dokumentiert werden.

Das Loeschkonzept ist fester Bestandteil des Datenschutz-Managementsystems und wird regelmaessig ueberprueft und aktualisiert.

` // ========================================================================= // Section 2: Geltungsbereich // ========================================================================= const storageListHtml = allStorageLocations.size > 0 ? Array.from(allStorageLocations).map(s => `
  • ${escHtml(s)}
  • `).join('') : '
  • Keine Speicherorte dokumentiert
  • ' html += `
    2. Geltungsbereich

    Dieses Loeschkonzept gilt fuer alle personenbezogenen Daten, die von ${escHtml(orgName)} verarbeitet werden. Es umfasst ${activePolicies.length} Loeschregeln fuer folgende Systeme und Speicherorte:

      ${storageListHtml}

    Saemtliche Verarbeitungstaetigkeiten, die im Verzeichnis von Verarbeitungstaetigkeiten (VVT) erfasst sind, werden durch dieses Loeschkonzept abgedeckt.

    ` // ========================================================================= // Section 3: Grundprinzipien // ========================================================================= html += `
    3. Grundprinzipien der Datenspeicherung
    Speicherbegrenzung: Personenbezogene Daten werden nur so lange gespeichert, wie es fuer den jeweiligen Verarbeitungszweck erforderlich ist (Art. 5 Abs. 1 lit. e DSGVO).
    3-Level-Loeschlogik: Die Loeschung folgt einer dreistufigen Priorisierung: (1) Zweckende, (2) gesetzliche Aufbewahrungspflichten, (3) Legal Hold — jeweils mit der laengsten Frist als massgeblich.
    Dokumentationspflicht: Jede Loeschregel ist dokumentiert mit Rechtsgrundlage, Frist, Loeschmethode und Verantwortlichkeit.
    Regelmaessige Ueberpruefung: Alle Loeschregeln werden im definierten Intervall ueberprueft und bei Bedarf angepasst.
    Datenschutz durch Technikgestaltung: Loeschmechanismen werden moeglichst automatisiert, um menschliche Fehler zu minimieren (Art. 25 DSGVO).
    ` // ========================================================================= // Section 4: Loeschregeln-Uebersicht // ========================================================================= html += `
    4. Loeschregeln-Uebersicht

    Die folgende Tabelle zeigt eine Uebersicht aller ${activePolicies.length} aktiven Loeschregeln:

    ` for (const p of activePolicies) { const trigger = TRIGGER_LABELS[getEffectiveDeletionTrigger(p)] const duration = formatRetentionDuration(p.retentionDuration, p.retentionUnit) const method = DELETION_METHOD_LABELS[p.deletionMethod] const statusLabel = STATUS_LABELS[p.status] const statusClass = p.status === 'ACTIVE' ? 'badge-active' : p.status === 'REVIEW_NEEDED' ? 'badge-review' : 'badge-draft' html += ` ` } html += `
    LF-Nr. Datenobjekt Loeschtrigger Aufbewahrungsfrist Loeschmethode Status
    ${escHtml(p.policyId)} ${escHtml(p.dataObjectName)} ${escHtml(trigger)} ${escHtml(duration)} ${escHtml(method)} ${escHtml(statusLabel)}
    ` // ========================================================================= // Section 5: Detaillierte Loeschregeln // ========================================================================= html += `
    5. Detaillierte Loeschregeln
    ` for (const p of activePolicies) { const trigger = TRIGGER_LABELS[getEffectiveDeletionTrigger(p)] const duration = formatRetentionDuration(p.retentionDuration, p.retentionUnit) const method = DELETION_METHOD_LABELS[p.deletionMethod] const statusLabel = STATUS_LABELS[p.status] const driverLabel = p.retentionDriver ? RETENTION_DRIVER_META[p.retentionDriver]?.label || p.retentionDriver : '-' const driverStatute = p.retentionDriver ? RETENTION_DRIVER_META[p.retentionDriver]?.statute || '' : '' const locations = p.storageLocations.map(l => l.name || l.type).join(', ') || '-' const responsible = [p.responsiblePerson, p.responsibleRole].filter(s => s.trim()).join(' / ') || '-' const activeHolds = getActiveLegalHolds(p) html += `
    ${escHtml(p.policyId)} — ${escHtml(p.dataObjectName)} ${escHtml(statusLabel)}
    ${activeHolds.length > 0 ? `` : ''}
    Beschreibung${escHtml(p.description || '-')}
    Betroffenengruppen${escHtml(p.affectedGroups.join(', ') || '-')}
    Datenkategorien${escHtml(p.dataCategories.join(', ') || '-')}
    Verarbeitungszweck${escHtml(p.primaryPurpose || '-')}
    Loeschtrigger${escHtml(trigger)}
    Aufbewahrungstreiber${escHtml(driverLabel)}${driverStatute ? ` (${escHtml(driverStatute)})` : ''}
    Aufbewahrungsfrist${escHtml(duration)}
    Startereignis${escHtml(p.startEvent || '-')}
    Loeschmethode${escHtml(method)}
    Loeschmethode (Detail)${escHtml(p.deletionMethodDetail || '-')}
    Speicherorte${escHtml(locations)}
    Verantwortlich${escHtml(responsible)}
    Pruefintervall${escHtml(REVIEW_INTERVAL_LABELS[p.reviewInterval] || p.reviewInterval)}
    Aktive Legal Holds${activeHolds.map(h => `${escHtml(h.reason)} (seit ${formatDateDE(h.startDate)})`).join('
    ')}
    ` } html += `
    ` // ========================================================================= // Section 6: VVT-Verknuepfung // ========================================================================= html += `
    6. VVT-Verknuepfung

    Die folgende Tabelle zeigt die Verknuepfung zwischen Loeschregeln und Verarbeitungstaetigkeiten im VVT (Art. 30 DSGVO):

    ` if (vvtRefs.length > 0) { html += ` ` for (const ref of vvtRefs) { html += ` ` } html += `
    LoeschregelLF-Nr.VVT-Nr.Verarbeitungstaetigkeit
    ${escHtml(ref.policyName)} ${escHtml(ref.policyId)} ${escHtml(ref.vvtId)} ${escHtml(ref.vvtName)}
    ` } else { html += `

    Noch keine VVT-Verknuepfungen dokumentiert. Verknuepfen Sie Ihre Loeschregeln mit den entsprechenden Verarbeitungstaetigkeiten im Editor-Tab.

    ` } html += `
    ` // ========================================================================= // Section 7: Auftragsverarbeiter mit Loeschpflichten // ========================================================================= html += `
    7. Auftragsverarbeiter mit Loeschpflichten

    Die folgende Tabelle zeigt Loeschregeln, die mit Auftragsverarbeitern verknuepft sind. Diese Verknuepfungen stellen sicher, dass auch bei extern verarbeiteten Daten die Loeschpflichten eingehalten werden (Art. 28 DSGVO).

    ` if (vendorRefs.length > 0) { html += ` ` for (const ref of vendorRefs) { html += ` ` } html += `
    LoeschregelLF-Nr.Auftragsverarbeiter (ID)Aufbewahrungsfrist
    ${escHtml(ref.policyName)} ${escHtml(ref.policyId)} ${escHtml(ref.vendorId)} ${escHtml(ref.duration)}
    ` } else { html += `

    Noch keine Auftragsverarbeiter mit Loeschregeln verknuepft. Verknuepfen Sie Ihre Loeschregeln mit den entsprechenden Auftragsverarbeitern im Editor-Tab.

    ` } html += `
    ` // ========================================================================= // Section 8: Legal Hold Verfahren // ========================================================================= html += `
    8. Legal Hold Verfahren

    Ein Legal Hold (Aufbewahrungspflicht aufgrund rechtlicher Verfahren) setzt die regulaere Loeschung aus. Betroffene Daten duerfen trotz abgelaufener Loeschfrist nicht geloescht werden, bis der Legal Hold aufgehoben wird.

    Verfahrensschritte:

    1. Rechtsabteilung/DSB identifiziert betroffene Datenkategorien
    2. Legal Hold wird im System aktiviert (Status: Aktiv)
    3. Automatische Loeschung wird fuer betroffene Policies ausgesetzt
    4. Regelmaessige Pruefung, ob der Legal Hold noch erforderlich ist
    5. Nach Aufhebung: Regulaere Loeschfristen greifen wieder
    ` if (allActiveLegalHolds.length > 0) { html += `

    Aktuell aktive Legal Holds (${allActiveLegalHolds.length}):

    ` for (const { policy, hold } of allActiveLegalHolds) { html += ` ` } html += `
    DatenobjektGrundRechtsgrundlageSeitVoraussichtlich bis
    ${escHtml(policy)} ${escHtml(hold.reason)} ${escHtml(hold.legalBasis)} ${formatDateDE(hold.startDate)} ${hold.expectedEndDate ? formatDateDE(hold.expectedEndDate) : 'Unbefristet'}
    ` } else { html += `

    Derzeit sind keine aktiven Legal Holds vorhanden.

    ` } html += `
    ` // ========================================================================= // Section 9: Verantwortlichkeiten // ========================================================================= html += `
    9. Verantwortlichkeiten

    Die folgende Rollenmatrix zeigt, welche Organisationseinheiten fuer welche Datenobjekte die Loeschverantwortung tragen:

    ` for (const [role, objects] of roleMap.entries()) { html += ` ` } html += `
    Rolle / VerantwortlichDatenobjekteAnzahl
    ${escHtml(role)} ${objects.map(o => escHtml(o)).join(', ')} ${objects.length}
    ` // ========================================================================= // Section 10: Pruef- und Revisionszyklus // ========================================================================= html += `
    10. Pruef- und Revisionszyklus
    EigenschaftWert
    Aktuelles Pruefintervall${escHtml(orgHeader.reviewInterval)}
    Letzte Pruefung${formatDateDE(orgHeader.lastReviewDate)}
    Naechste Pruefung${formatDateDE(orgHeader.nextReviewDate)}
    Aktuelle Version${escHtml(orgHeader.loeschkonzeptVersion)}

    Bei jeder Pruefung wird das Loeschkonzept auf folgende Punkte ueberprueft:

    • Vollstaendigkeit aller Loeschregeln (neue Verarbeitungen erfasst?)
    • Aktualitaet der gesetzlichen Aufbewahrungsfristen
    • Wirksamkeit der technischen Loeschmechanismen
    • Einhaltung der definierten Loeschfristen
    • Angemessenheit der Verantwortlichkeiten
    ` // ========================================================================= // Section 11: Compliance-Status // ========================================================================= html += `
    11. Compliance-Status
    ` if (complianceResult) { const scoreClass = complianceResult.score >= 90 ? 'score-excellent' : complianceResult.score >= 75 ? 'score-good' : complianceResult.score >= 50 ? 'score-needs-work' : 'score-poor' const scoreLabel = complianceResult.score >= 90 ? 'Ausgezeichnet' : complianceResult.score >= 75 ? 'Gut' : complianceResult.score >= 50 ? 'Verbesserungswuerdig' : 'Mangelhaft' html += `

    ${complianceResult.score}/100 ${escHtml(scoreLabel)}

    KennzahlWert
    Gepruefte Policies${complianceResult.stats.total}
    Bestanden${complianceResult.stats.passed}
    Beanstandungen${complianceResult.stats.failed}
    ` if (complianceResult.issues.length > 0) { html += `

    Befunde nach Schweregrad:

    ` const severityOrder: ComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] for (const sev of severityOrder) { const count = complianceResult.stats.bySeverity[sev] if (count === 0) continue const issuesForSev = complianceResult.issues.filter(i => i.severity === sev) html += ` ` } html += `
    SchweregradAnzahlBefunde
    ${SEVERITY_LABELS_DE[sev]} ${count} ${issuesForSev.map(i => escHtml(i.title)).join('; ')}
    ` } else { html += `

    Keine Beanstandungen. Alle Policies sind konform.

    ` } } else { html += `

    Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im Export-Tab durch, um den Status in das Dokument aufzunehmen.

    ` } html += `
    ` // ========================================================================= // Section 12: Aenderungshistorie // ========================================================================= html += `
    12. Aenderungshistorie
    ` if (revisions.length > 0) { for (const rev of revisions) { html += ` ` } } else { html += ` ` } html += `
    VersionDatumAutorAenderungen
    ${escHtml(rev.version)} ${formatDateDE(rev.date)} ${escHtml(rev.author)} ${escHtml(rev.changes)}
    ${escHtml(orgHeader.loeschkonzeptVersion)} ${today} ${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '-')} Erstversion des Loeschkonzepts
    ` // ========================================================================= // Footer // ========================================================================= html += ` ` return html } // ============================================================================= // INTERNAL HELPERS // ============================================================================= function escHtml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') } 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 '-' } }