// ============================================================================= // 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 } from '../loeschfristen-compliance' import type { LoeschkonzeptOrgHeader, LoeschkonzeptRevision } from './types-defaults' import { escHtml, formatDateDE } from './helpers' import { buildSections6to9, buildSections10to12 } from './html-builder-sections-6-12' 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 }