// ============================================================================= // Obligations Document — HTML Document Builder // ============================================================================= import type { Obligation, ObligationComplianceCheckResult, ObligationComplianceIssueSeverity } from '../obligations-compliance' import { OBLIGATION_SEVERITY_LABELS_DE, OBLIGATION_SEVERITY_COLORS } from '../obligations-compliance' import type { ObligationDocumentOrgHeader, ObligationDocumentRevision } from './types-defaults' import { STATUS_LABELS_DE, STATUS_BADGE_CLASSES, PRIORITY_LABELS_DE, PRIORITY_BADGE_CLASSES, } from './types-defaults' import { escHtml, formatDateDE, daysBetween } from './helpers' import { getDocumentStyles } from './html-styles' export function buildObligationDocumentHtml( obligations: Obligation[], orgHeader: ObligationDocumentOrgHeader, complianceResult: ObligationComplianceCheckResult | null, revisions: ObligationDocumentRevision[] ): string { const today = new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }) const orgName = orgHeader.organizationName || 'Organisation' // Group obligations by source (regulation) const bySource = new Map() for (const o of obligations) { const src = o.source || 'Sonstig' if (!bySource.has(src)) bySource.set(src, []) bySource.get(src)!.push(o) } // Build role map const roleMap = new Map() for (const o of obligations) { const role = o.responsible || 'Nicht zugewiesen' if (!roleMap.has(role)) roleMap.set(role, []) roleMap.get(role)!.push(o) } const distinctSources = Array.from(bySource.keys()).sort() let html = getDocumentStyles(`Pflichtenregister — ${escHtml(orgName)}`) html += buildCoverPage(orgHeader, orgName, today) html += buildTOC() html += buildSection1(orgName) html += buildSection2(orgName, orgHeader, bySource, obligations, distinctSources) html += buildSection3() html += buildSection4(bySource, obligations) html += buildSection5(bySource) html += buildSection6(bySource) html += buildSection7(roleMap) html += buildSection8(obligations, today) html += buildSection9(obligations) html += buildSection10(complianceResult) html += buildSection11(revisions, orgHeader, today) html += buildFooter(orgName, today, orgHeader.documentVersion) return html } function buildCoverPage( orgHeader: ObligationDocumentOrgHeader, orgName: string, today: string ): string { return `

Pflichtenregister

Regulatorische Pflichten — DSGVO, AI Act, NIS2 und weitere
Organisation: ${escHtml(orgName)}
${orgHeader.industry ? `
Branche: ${escHtml(orgHeader.industry)}
` : ''} ${orgHeader.dpoName ? `
DSB: ${escHtml(orgHeader.dpoName)}
` : ''} ${orgHeader.dpoContact ? `
DSB-Kontakt: ${escHtml(orgHeader.dpoContact)}
` : ''} ${orgHeader.responsiblePerson ? `
Verantwortlicher: ${escHtml(orgHeader.responsiblePerson)}
` : ''} ${orgHeader.legalDepartment ? `
Rechtsabteilung: ${escHtml(orgHeader.legalDepartment)}
` : ''}
` } function buildTOC(): string { const sections = [ 'Ziel und Zweck', 'Geltungsbereich', 'Methodik', 'Regulatorische Grundlagen', 'Pflichtenuebersicht', 'Detaillierte Pflichten', 'Verantwortlichkeiten', 'Fristen und Termine', 'Nachweisverzeichnis', 'Compliance-Status', 'Aenderungshistorie', ] return `

Inhaltsverzeichnis

${sections.map((s, i) => `
${i + 1}. ${escHtml(s)}
`).join('\n ')}
` } function buildSection1(orgName: string): string { return `
1. Ziel und Zweck

Dieses Pflichtenregister dokumentiert alle regulatorischen Pflichten, denen ${escHtml(orgName)} unterliegt. Es dient der systematischen Erfassung, Ueberwachung und Nachverfolgung aller Compliance-Anforderungen aus den anwendbaren Regulierungen.

Das Register erfuellt folgende Zwecke:

  • Vollstaendige Erfassung aller anwendbaren regulatorischen Pflichten
  • Zuordnung von Verantwortlichkeiten und Fristen
  • Nachverfolgung des Umsetzungsstatus
  • Dokumentation von Nachweisen fuer Audits
  • Identifikation von Compliance-Luecken und Handlungsbedarf
RechtsrahmenRelevanz
DSGVO (EU) 2016/679Datenschutz-Grundverordnung — Kernregulierung fuer personenbezogene Daten
AI Act (EU) 2024/1689KI-Verordnung — Anforderungen an KI-Systeme nach Risikoklasse
NIS2 (EU) 2022/2555Netzwerk- und Informationssicherheit — Cybersicherheitspflichten
BDSGBundesdatenschutzgesetz — Nationale Ergaenzung zur DSGVO
` } function buildSection2( orgName: string, orgHeader: ObligationDocumentOrgHeader, bySource: Map, obligations: Obligation[], distinctSources: string[] ): string { let html = `
2. Geltungsbereich

Dieses Pflichtenregister gilt fuer alle Geschaeftsprozesse und IT-Systeme von ${escHtml(orgName)}${orgHeader.industry ? ` (Branche: ${escHtml(orgHeader.industry)})` : ''}.

Anwendbare Regulierungen:

` for (const [source, obls] of bySource.entries()) { const completed = obls.filter(o => o.status === 'completed').length const pct = obls.length > 0 ? Math.round((completed / obls.length) * 100) : 0 html += ` ` } html += `
RegulierungAnzahl PflichtenStatus
${escHtml(source)} ${obls.length} ${completed}/${obls.length} abgeschlossen (${pct}%)

Insgesamt umfasst dieses Register ${obligations.length} Pflichten aus ${distinctSources.length} Regulierungen.

` return html } function buildSection3(): string { return `
3. Methodik

Die Identifikation und Bewertung der Pflichten erfolgt in drei Schritten:

Pflicht-Identifikation: Systematische Analyse aller anwendbaren Regulierungen und Extraktion der einzelnen Pflichten mit Artikel-Referenz, Beschreibung und Zielgruppe.
Bewertung und Priorisierung: Jede Pflicht wird nach Prioritaet (kritisch, hoch, mittel, niedrig) und Dringlichkeit (Frist) bewertet. Die Bewertung basiert auf dem Risikopotenzial bei Nichterfuellung.
Ueberwachung und Nachverfolgung: Regelmaessige Pruefung des Umsetzungsstatus, Aktualisierung der Fristen und Dokumentation von Nachweisen.

Die Pflichten werden ueber einen automatisierten Compliance-Check geprueft, der 11 Kriterien umfasst (siehe Abschnitt 10: Compliance-Status).

` } function buildSection4(bySource: Map, obligations: Obligation[]): string { const totalCritical = obligations.filter(o => o.priority === 'critical').length const totalHigh = obligations.filter(o => o.priority === 'high').length const totalMedium = obligations.filter(o => o.priority === 'medium').length const totalLow = obligations.filter(o => o.priority === 'low').length const totalCompleted = obligations.filter(o => o.status === 'completed').length let html = `
4. Regulatorische Grundlagen

Die folgende Tabelle zeigt die regulatorischen Grundlagen mit Artikelzahl und Umsetzungsstatus:

` for (const [source, obls] of bySource.entries()) { const critical = obls.filter(o => o.priority === 'critical').length const high = obls.filter(o => o.priority === 'high').length const medium = obls.filter(o => o.priority === 'medium').length const low = obls.filter(o => o.priority === 'low').length const completed = obls.filter(o => o.status === 'completed').length html += ` ` } html += `
Regulierung Pflichten Kritisch Hoch Mittel Niedrig Abgeschlossen
${escHtml(source)} ${obls.length} ${critical} ${high} ${medium} ${low} ${completed}
Gesamt ${obligations.length} ${totalCritical} ${totalHigh} ${totalMedium} ${totalLow} ${totalCompleted}
` return html } function buildSection5(bySource: Map): string { let html = `
5. Pflichtenuebersicht

Uebersicht aller Pflichten nach Regulierung und Status:

` for (const [source, obls] of bySource.entries()) { const pending = obls.filter(o => o.status === 'pending').length const inProgress = obls.filter(o => o.status === 'in-progress').length const completed = obls.filter(o => o.status === 'completed').length const overdue = obls.filter(o => o.status === 'overdue').length html += ` ` } html += `
Regulierung Gesamt Ausstehend In Bearbeitung Abgeschlossen Ueberfaellig
${escHtml(source)} ${obls.length} ${pending} ${inProgress} ${completed} ${overdue > 0 ? `${overdue}` : '0'}
` return html } function buildSection6(bySource: Map): string { const priorityOrder: Record = { critical: 0, high: 1, medium: 2, low: 3 } let html = `
6. Detaillierte Pflichten
` for (const [source, obls] of bySource.entries()) { const sorted = [...obls].sort((a, b) => { const pa = priorityOrder[a.priority] ?? 2 const pb = priorityOrder[b.priority] ?? 2 if (pa !== pb) return pa - pb return a.title.localeCompare(b.title) }) html += `

${escHtml(source)} (${sorted.length} Pflichten)

` for (const o of sorted) { const statusLabel = STATUS_LABELS_DE[o.status] || o.status const statusBadge = STATUS_BADGE_CLASSES[o.status] || 'badge-draft' const priorityLabel = PRIORITY_LABELS_DE[o.priority] || o.priority const priorityBadge = PRIORITY_BADGE_CLASSES[o.priority] || 'badge-draft' const deadlineStr = o.deadline ? formatDateDE(o.deadline) : '—' const evidenceStr = o.evidence && o.evidence.length > 0 ? o.evidence.map(e => escHtml(e)).join(', ') : 'Kein Nachweis' const systemsStr = o.linked_systems && o.linked_systems.length > 0 ? o.linked_systems.map(s => escHtml(s)).join(', ') : '—' html += `
${escHtml(o.title)} ${escHtml(statusLabel)}
${o.linked_vendor_ids && o.linked_vendor_ids.length > 0 ? `` : ''} ${o.notes ? `` : ''}
Rechtsquelle${escHtml(o.source)} ${escHtml(o.source_article || '')}
Beschreibung${escHtml(o.description || '—')}
Prioritaet${escHtml(priorityLabel)}
Status${escHtml(statusLabel)}
Verantwortlich${escHtml(o.responsible || '—')}
Frist${deadlineStr}
Nachweise${evidenceStr}
Betroffene Systeme${systemsStr}
Auftragsverarbeiter${o.linked_vendor_ids.map(id => escHtml(id)).join(', ')}
Notizen${escHtml(o.notes)}
` } } html += `
` return html } function buildSection7(roleMap: Map): string { let html = `
7. Verantwortlichkeiten

Die folgende Rollenmatrix zeigt, welche Personen oder Abteilungen fuer welche Pflichten die Umsetzungsverantwortung tragen:

` for (const [role, obls] of roleMap.entries()) { const openCount = obls.filter(o => o.status !== 'completed').length const titles = obls.slice(0, 5).map(o => escHtml(o.title)) const suffix = obls.length > 5 ? `, ... (+${obls.length - 5})` : '' html += ` ` } html += `
VerantwortlichPflichtenAnzahlDavon offen
${escHtml(role)} ${titles.join('; ')}${suffix} ${obls.length} ${openCount}
` return html } function buildSection8(obligations: Obligation[], _today: string): string { const now = new Date() const withDeadline = obligations .filter(o => o.deadline && o.status !== 'completed') .sort((a, b) => new Date(a.deadline!).getTime() - new Date(b.deadline!).getTime()) const overdue = withDeadline.filter(o => new Date(o.deadline!) < now) const upcoming = withDeadline.filter(o => new Date(o.deadline!) >= now) let html = `
8. Fristen und Termine
` if (overdue.length > 0) { html += `

Ueberfaellige Pflichten (${overdue.length})

` for (const o of overdue) { const days = daysBetween(new Date(o.deadline!), now) html += ` ` } html += `
PflichtRegulierungFristTage ueberfaelligPrioritaet
${escHtml(o.title)} ${escHtml(o.source)} ${formatDateDE(o.deadline)} ${days} Tage ${escHtml(PRIORITY_LABELS_DE[o.priority] || o.priority)}
` } if (upcoming.length > 0) { html += `

Anstehende Fristen (${upcoming.length})

` for (const o of upcoming.slice(0, 20)) { const days = daysBetween(now, new Date(o.deadline!)) html += ` ` } if (upcoming.length > 20) { html += ` ` } html += `
PflichtRegulierungFristVerbleibendVerantwortlich
${escHtml(o.title)} ${escHtml(o.source)} ${formatDateDE(o.deadline)} ${days} Tage ${escHtml(o.responsible || '—')}
... und ${upcoming.length - 20} weitere
` } if (withDeadline.length === 0) { html += `

Keine offenen Pflichten mit Fristen vorhanden.

` } html += `
` return html } function buildSection9(obligations: Obligation[]): string { const withEvidence = obligations.filter(o => o.evidence && o.evidence.length > 0) const withoutEvidence = obligations.filter(o => !o.evidence || o.evidence.length === 0) let html = `
9. Nachweisverzeichnis

${withEvidence.length} von ${obligations.length} Pflichten haben Nachweise hinterlegt.

` if (withEvidence.length > 0) { html += ` ` for (const o of withEvidence) { html += ` ` } html += `
PflichtRegulierungNachweiseStatus
${escHtml(o.title)} ${escHtml(o.source)} ${o.evidence!.map(e => escHtml(e)).join(', ')} ${escHtml(STATUS_LABELS_DE[o.status] || o.status)}
` } if (withoutEvidence.length > 0) { html += `

Pflichten ohne Nachweise (${withoutEvidence.length}):

    ` for (const o of withoutEvidence.slice(0, 15)) { html += `
  • ${escHtml(o.title)} (${escHtml(o.source)})
  • ` } if (withoutEvidence.length > 15) { html += `
  • ... und ${withoutEvidence.length - 15} weitere
  • ` } html += `
` } html += `
` return html } function buildSection10(complianceResult: ObligationComplianceCheckResult | null): string { let html = `
10. 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
Geprueft am${formatDateDE(complianceResult.checkedAt)}
Befunde gesamt${complianceResult.summary.total}
Kritisch${complianceResult.summary.critical}
Hoch${complianceResult.summary.high}
Mittel${complianceResult.summary.medium}
Niedrig${complianceResult.summary.low}
` if (complianceResult.issues.length > 0) { html += `

Befunde nach Schweregrad:

` const severityOrder: ObligationComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] for (const sev of severityOrder) { const issuesForSev = complianceResult.issues.filter(i => i.severity === sev) for (const issue of issuesForSev) { html += ` ` } } html += `
SchweregradBefundBetroffene PflichtenEmpfehlung
${OBLIGATION_SEVERITY_LABELS_DE[sev]} ${escHtml(issue.message)} ${issue.affectedObligations.length > 0 ? issue.affectedObligations.length + ' Pflicht(en)' : '—'} ${escHtml(issue.recommendation)}
` } else { html += `

Keine Beanstandungen. Alle Pflichten sind konform.

` } } else { html += `

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

` } html += `
` return html } function buildSection11( revisions: ObligationDocumentRevision[], orgHeader: ObligationDocumentOrgHeader, today: string ): string { let html = `
11. 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.documentVersion)} ${today} ${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '—')} Erstversion des Pflichtenregisters
` return html } function buildFooter(orgName: string, today: string, version: string): string { return ` ` }