// ============================================================================= // Loeschfristen Document — HTML Document Builder // ============================================================================= import type { LoeschfristPolicy, } 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' import type { LoeschkonzeptOrgHeader, LoeschkonzeptRevision } from './types-defaults' import { SEVERITY_LABELS_DE, SEVERITY_COLORS } from './types-defaults' import { escHtml, formatDateDE } from './helpers' type VVTActivity = { id: string; vvt_id?: string; vvtId?: string; name?: string; activity_name?: string } export function buildLoeschkonzeptHtml( policies: LoeschfristPolicy[], orgHeader: LoeschkonzeptOrgHeader, vvtActivities: VVTActivity[], 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 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), }) } } } let html = buildDocumentHeader(orgName) html += buildCoverAndTOC(orgHeader, orgName, today) html += buildSections1to5(orgName, orgHeader, activePolicies, allStorageLocations) html += buildSections6to9(activePolicies, vvtRefs, vendorRefs, allActiveLegalHolds, roleMap) html += buildSections10to12(orgHeader, complianceResult, revisions, today) html += ` ` return html } function buildDocumentHeader(orgName: string): string { return ` Loeschkonzept — ${escHtml(orgName)} ` } function buildCoverAndTOC( orgHeader: LoeschkonzeptOrgHeader, orgName: string, today: string ): string { 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', ] return `

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(', '))}
` : ''}

Inhaltsverzeichnis

${sections.map((s, i) => `
${i + 1}. ${escHtml(s)}
`).join('\n ')}
` } function buildSections1to5( orgName: string, orgHeader: LoeschkonzeptOrgHeader, activePolicies: LoeschfristPolicy[], allStorageLocations: Set ): string { const storageListHtml = allStorageLocations.size > 0 ? Array.from(allStorageLocations).map(s => `
  • ${escHtml(s)}
  • `).join('') : '
  • Keine Speicherorte dokumentiert
  • ' return `
    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.

    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.

    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).
    ${buildSection4Overview(activePolicies)} ${buildSection5Detail(activePolicies)} ` } function buildSection4Overview(activePolicies: LoeschfristPolicy[]): string { let 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)}
    ` return html } function buildSection5Detail(activePolicies: LoeschfristPolicy[]): string { let 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 += `
    ` return html } function buildSections6to9( activePolicies: LoeschfristPolicy[], vvtRefs: Array<{ policyName: string; policyId: string; vvtId: string; vvtName: string }>, vendorRefs: Array<{ policyName: string; policyId: string; vendorId: string; duration: string }>, allActiveLegalHolds: Array<{ policy: string; hold: LoeschfristPolicy['legalHolds'][0] }>, roleMap: Map ): string { let 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.

    ` } html += `
    7. Auftragsverarbeiter mit Loeschpflichten

    Die folgende Tabelle zeigt Loeschregeln, die mit Auftragsverarbeitern verknuepft sind (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.

    ` } html += `
    8. Legal Hold Verfahren

    Ein Legal Hold 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 += `
    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}
    ` return html } function buildSections10to12( orgHeader: LoeschkonzeptOrgHeader, complianceResult: ComplianceCheckResult | null, revisions: LoeschkonzeptRevision[], today: string ): string { let 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
    ` html += buildSection11Compliance(complianceResult) html += buildSection12History(revisions, orgHeader, today) return html } function buildSection11Compliance(complianceResult: ComplianceCheckResult | null): string { let 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.

    ` } html += `
    ` return html } function buildSection12History( revisions: LoeschkonzeptRevision[], orgHeader: LoeschkonzeptOrgHeader, today: string ): string { let 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
    ` return html }