obligations-document/html-builder.ts (620→304 LOC): extract sections 6-11 and footer into html-builder-sections-6-11.ts (339 LOC). loeschfristen-document/html-builder.ts (603→353 LOC): extract sections 6-12 into html-builder-sections-6-12.ts (259 LOC). Both orchestrators re-export from siblings; zero behavior change. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
260 lines
9.2 KiB
TypeScript
260 lines
9.2 KiB
TypeScript
// =============================================================================
|
||
// Loeschfristen Document — HTML Builder: Sections 6–12
|
||
// =============================================================================
|
||
|
||
import type { LoeschfristPolicy } 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'
|
||
|
||
export 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, string[]>
|
||
): string {
|
||
let html = `
|
||
<div class="section page-break">
|
||
<div class="section-header">6. VVT-Verknuepfung</div>
|
||
<div class="section-body">
|
||
<p>Die folgende Tabelle zeigt die Verknuepfung zwischen Loeschregeln und Verarbeitungstaetigkeiten im VVT (Art. 30 DSGVO):</p>
|
||
`
|
||
if (vvtRefs.length > 0) {
|
||
html += ` <table>
|
||
<tr><th>Loeschregel</th><th>LF-Nr.</th><th>VVT-Nr.</th><th>Verarbeitungstaetigkeit</th></tr>
|
||
`
|
||
for (const ref of vvtRefs) {
|
||
html += ` <tr>
|
||
<td>${escHtml(ref.policyName)}</td>
|
||
<td>${escHtml(ref.policyId)}</td>
|
||
<td>${escHtml(ref.vvtId)}</td>
|
||
<td>${escHtml(ref.vvtName)}</td>
|
||
</tr>
|
||
`
|
||
}
|
||
html += ` </table>
|
||
`
|
||
} else {
|
||
html += ` <p><em>Noch keine VVT-Verknuepfungen dokumentiert.</em></p>
|
||
`
|
||
}
|
||
html += ` </div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">7. Auftragsverarbeiter mit Loeschpflichten</div>
|
||
<div class="section-body">
|
||
<p>Die folgende Tabelle zeigt Loeschregeln, die mit Auftragsverarbeitern verknuepft sind (Art. 28 DSGVO).</p>
|
||
`
|
||
if (vendorRefs.length > 0) {
|
||
html += ` <table>
|
||
<tr><th>Loeschregel</th><th>LF-Nr.</th><th>Auftragsverarbeiter (ID)</th><th>Aufbewahrungsfrist</th></tr>
|
||
`
|
||
for (const ref of vendorRefs) {
|
||
html += ` <tr>
|
||
<td>${escHtml(ref.policyName)}</td>
|
||
<td>${escHtml(ref.policyId)}</td>
|
||
<td>${escHtml(ref.vendorId)}</td>
|
||
<td>${escHtml(ref.duration)}</td>
|
||
</tr>
|
||
`
|
||
}
|
||
html += ` </table>
|
||
`
|
||
} else {
|
||
html += ` <p><em>Noch keine Auftragsverarbeiter mit Loeschregeln verknuepft.</em></p>
|
||
`
|
||
}
|
||
html += ` </div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">8. Legal Hold Verfahren</div>
|
||
<div class="section-body">
|
||
<p>Ein Legal Hold setzt die regulaere Loeschung aus. Betroffene Daten duerfen trotz abgelaufener Loeschfrist nicht geloescht werden, bis der Legal Hold aufgehoben wird.</p>
|
||
<p><strong>Verfahrensschritte:</strong></p>
|
||
<ol style="margin: 8px 0 8px 24px;">
|
||
<li>Rechtsabteilung/DSB identifiziert betroffene Datenkategorien</li>
|
||
<li>Legal Hold wird im System aktiviert (Status: Aktiv)</li>
|
||
<li>Automatische Loeschung wird fuer betroffene Policies ausgesetzt</li>
|
||
<li>Regelmaessige Pruefung, ob der Legal Hold noch erforderlich ist</li>
|
||
<li>Nach Aufhebung: Regulaere Loeschfristen greifen wieder</li>
|
||
</ol>
|
||
`
|
||
if (allActiveLegalHolds.length > 0) {
|
||
html += ` <p><strong>Aktuell aktive Legal Holds (${allActiveLegalHolds.length}):</strong></p>
|
||
<table>
|
||
<tr><th>Datenobjekt</th><th>Grund</th><th>Rechtsgrundlage</th><th>Seit</th><th>Voraussichtlich bis</th></tr>
|
||
`
|
||
for (const { policy, hold } of allActiveLegalHolds) {
|
||
html += ` <tr>
|
||
<td>${escHtml(policy)}</td>
|
||
<td>${escHtml(hold.reason)}</td>
|
||
<td>${escHtml(hold.legalBasis)}</td>
|
||
<td>${formatDateDE(hold.startDate)}</td>
|
||
<td>${hold.expectedEndDate ? formatDateDE(hold.expectedEndDate) : 'Unbefristet'}</td>
|
||
</tr>
|
||
`
|
||
}
|
||
html += ` </table>
|
||
`
|
||
} else {
|
||
html += ` <p><em>Derzeit sind keine aktiven Legal Holds vorhanden.</em></p>
|
||
`
|
||
}
|
||
html += ` </div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">9. Verantwortlichkeiten</div>
|
||
<div class="section-body">
|
||
<p>Die folgende Rollenmatrix zeigt, welche Organisationseinheiten fuer welche Datenobjekte die Loeschverantwortung tragen:</p>
|
||
<table>
|
||
<tr><th>Rolle / Verantwortlich</th><th>Datenobjekte</th><th>Anzahl</th></tr>
|
||
`
|
||
for (const [role, objects] of roleMap.entries()) {
|
||
html += ` <tr>
|
||
<td>${escHtml(role)}</td>
|
||
<td>${objects.map(o => escHtml(o)).join(', ')}</td>
|
||
<td>${objects.length}</td>
|
||
</tr>
|
||
`
|
||
}
|
||
html += ` </table>
|
||
</div>
|
||
</div>
|
||
`
|
||
return html
|
||
}
|
||
|
||
export function buildSections10to12(
|
||
orgHeader: LoeschkonzeptOrgHeader,
|
||
complianceResult: ComplianceCheckResult | null,
|
||
revisions: LoeschkonzeptRevision[],
|
||
today: string
|
||
): string {
|
||
let html = `
|
||
<div class="section">
|
||
<div class="section-header">10. Pruef- und Revisionszyklus</div>
|
||
<div class="section-body">
|
||
<table>
|
||
<tr><th>Eigenschaft</th><th>Wert</th></tr>
|
||
<tr><td>Aktuelles Pruefintervall</td><td>${escHtml(orgHeader.reviewInterval)}</td></tr>
|
||
<tr><td>Letzte Pruefung</td><td>${formatDateDE(orgHeader.lastReviewDate)}</td></tr>
|
||
<tr><td>Naechste Pruefung</td><td>${formatDateDE(orgHeader.nextReviewDate)}</td></tr>
|
||
<tr><td>Aktuelle Version</td><td>${escHtml(orgHeader.loeschkonzeptVersion)}</td></tr>
|
||
</table>
|
||
<p style="margin-top: 8px;">Bei jeder Pruefung wird das Loeschkonzept auf folgende Punkte ueberprueft:</p>
|
||
<ul style="margin: 8px 0 8px 24px;">
|
||
<li>Vollstaendigkeit aller Loeschregeln (neue Verarbeitungen erfasst?)</li>
|
||
<li>Aktualitaet der gesetzlichen Aufbewahrungsfristen</li>
|
||
<li>Wirksamkeit der technischen Loeschmechanismen</li>
|
||
<li>Einhaltung der definierten Loeschfristen</li>
|
||
<li>Angemessenheit der Verantwortlichkeiten</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
`
|
||
|
||
html += buildSection11Compliance(complianceResult)
|
||
html += buildSection12History(revisions, orgHeader, today)
|
||
return html
|
||
}
|
||
|
||
function buildSection11Compliance(complianceResult: ComplianceCheckResult | null): string {
|
||
let html = `
|
||
<div class="section page-break">
|
||
<div class="section-header">11. Compliance-Status</div>
|
||
<div class="section-body">
|
||
`
|
||
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 += ` <p><span class="score-box ${scoreClass}">${complianceResult.score}/100</span> ${escHtml(scoreLabel)}</p>
|
||
<table style="margin-top: 12px;">
|
||
<tr><th>Kennzahl</th><th>Wert</th></tr>
|
||
<tr><td>Gepruefte Policies</td><td>${complianceResult.stats.total}</td></tr>
|
||
<tr><td>Bestanden</td><td>${complianceResult.stats.passed}</td></tr>
|
||
<tr><td>Beanstandungen</td><td>${complianceResult.stats.failed}</td></tr>
|
||
</table>
|
||
`
|
||
if (complianceResult.issues.length > 0) {
|
||
html += ` <p style="margin-top: 12px;"><strong>Befunde nach Schweregrad:</strong></p>
|
||
<table>
|
||
<tr><th>Schweregrad</th><th>Anzahl</th><th>Befunde</th></tr>
|
||
`
|
||
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 += ` <tr>
|
||
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${SEVERITY_COLORS[sev]}">${SEVERITY_LABELS_DE[sev]}</span></td>
|
||
<td>${count}</td>
|
||
<td>${issuesForSev.map(i => escHtml(i.title)).join('; ')}</td>
|
||
</tr>
|
||
`
|
||
}
|
||
html += ` </table>
|
||
`
|
||
} else {
|
||
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Policies sind konform.</em></p>
|
||
`
|
||
}
|
||
} else {
|
||
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt.</em></p>
|
||
`
|
||
}
|
||
html += ` </div>
|
||
</div>
|
||
`
|
||
return html
|
||
}
|
||
|
||
function buildSection12History(
|
||
revisions: LoeschkonzeptRevision[],
|
||
orgHeader: LoeschkonzeptOrgHeader,
|
||
today: string
|
||
): string {
|
||
let html = `
|
||
<div class="section">
|
||
<div class="section-header">12. Aenderungshistorie</div>
|
||
<div class="section-body">
|
||
<table>
|
||
<tr><th>Version</th><th>Datum</th><th>Autor</th><th>Aenderungen</th></tr>
|
||
`
|
||
if (revisions.length > 0) {
|
||
for (const rev of revisions) {
|
||
html += ` <tr>
|
||
<td>${escHtml(rev.version)}</td>
|
||
<td>${formatDateDE(rev.date)}</td>
|
||
<td>${escHtml(rev.author)}</td>
|
||
<td>${escHtml(rev.changes)}</td>
|
||
</tr>
|
||
`
|
||
}
|
||
} else {
|
||
html += ` <tr>
|
||
<td>${escHtml(orgHeader.loeschkonzeptVersion)}</td>
|
||
<td>${today}</td>
|
||
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '-')}</td>
|
||
<td>Erstversion des Loeschkonzepts</td>
|
||
</tr>
|
||
`
|
||
}
|
||
html += ` </table>
|
||
</div>
|
||
</div>
|
||
`
|
||
return html
|
||
}
|