From 92a47bf6f94d314ad0a0b35d8ba1891a377f7b1f Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:12:01 +0200 Subject: [PATCH] refactor: split oversized html-builder files under 500 LOC hard cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../html-builder-sections-6-12.ts | 259 +++++++++++++ .../loeschfristen-document/html-builder.ts | 254 +------------ .../html-builder-sections-6-11.ts | 339 ++++++++++++++++++ .../sdk/obligations-document/html-builder.ts | 338 +---------------- 4 files changed, 611 insertions(+), 579 deletions(-) create mode 100644 admin-compliance/lib/sdk/loeschfristen-document/html-builder-sections-6-12.ts create mode 100644 admin-compliance/lib/sdk/obligations-document/html-builder-sections-6-11.ts diff --git a/admin-compliance/lib/sdk/loeschfristen-document/html-builder-sections-6-12.ts b/admin-compliance/lib/sdk/loeschfristen-document/html-builder-sections-6-12.ts new file mode 100644 index 0000000..2c5bf21 --- /dev/null +++ b/admin-compliance/lib/sdk/loeschfristen-document/html-builder-sections-6-12.ts @@ -0,0 +1,259 @@ +// ============================================================================= +// 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 { + 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. +
  3. Legal Hold wird im System aktiviert (Status: Aktiv)
  4. +
  5. Automatische Loeschung wird fuer betroffene Policies ausgesetzt
  6. +
  7. Regelmaessige Pruefung, ob der Legal Hold noch erforderlich ist
  8. +
  9. Nach Aufhebung: Regulaere Loeschfristen greifen wieder
  10. +
+` + 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 +} + +export 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 +} diff --git a/admin-compliance/lib/sdk/loeschfristen-document/html-builder.ts b/admin-compliance/lib/sdk/loeschfristen-document/html-builder.ts index 848f73a..189f29e 100644 --- a/admin-compliance/lib/sdk/loeschfristen-document/html-builder.ts +++ b/admin-compliance/lib/sdk/loeschfristen-document/html-builder.ts @@ -15,10 +15,10 @@ import { getEffectiveDeletionTrigger, getActiveLegalHolds, } from '../loeschfristen-types' -import type { ComplianceCheckResult, ComplianceIssueSeverity } from '../loeschfristen-compliance' +import type { ComplianceCheckResult } from '../loeschfristen-compliance' import type { LoeschkonzeptOrgHeader, LoeschkonzeptRevision } from './types-defaults' -import { SEVERITY_LABELS_DE, SEVERITY_COLORS } 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 } @@ -351,253 +351,3 @@ function buildSection5Detail(activePolicies: LoeschfristPolicy[]): string { ` 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. -
  3. Legal Hold wird im System aktiviert (Status: Aktiv)
  4. -
  5. Automatische Loeschung wird fuer betroffene Policies ausgesetzt
  6. -
  7. Regelmaessige Pruefung, ob der Legal Hold noch erforderlich ist
  8. -
  9. Nach Aufhebung: Regulaere Loeschfristen greifen wieder
  10. -
-` - 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 -} diff --git a/admin-compliance/lib/sdk/obligations-document/html-builder-sections-6-11.ts b/admin-compliance/lib/sdk/obligations-document/html-builder-sections-6-11.ts new file mode 100644 index 0000000..5c8da61 --- /dev/null +++ b/admin-compliance/lib/sdk/obligations-document/html-builder-sections-6-11.ts @@ -0,0 +1,339 @@ +// ============================================================================= +// Obligations Document — HTML Builder: Sections 6–11 +// ============================================================================= + +import type { Obligation, ObligationComplianceCheckResult, ObligationComplianceIssueSeverity } from '../obligations-compliance' +import { OBLIGATION_SEVERITY_LABELS_DE, OBLIGATION_SEVERITY_COLORS } from '../obligations-compliance' +import { + STATUS_LABELS_DE, + STATUS_BADGE_CLASSES, + PRIORITY_LABELS_DE, + PRIORITY_BADGE_CLASSES, +} from './types-defaults' +import { escHtml, formatDateDE, daysBetween } from './helpers' +import type { ObligationDocumentOrgHeader, ObligationDocumentRevision } from './types-defaults' + +export 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 +} + +export 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 +} + +export 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 +} + +export 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 +} + +export 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 +} + +export 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 +} + +export function buildFooter(orgName: string, today: string, version: string): string { + return ` + + + +` +} diff --git a/admin-compliance/lib/sdk/obligations-document/html-builder.ts b/admin-compliance/lib/sdk/obligations-document/html-builder.ts index eac02f7..8025585 100644 --- a/admin-compliance/lib/sdk/obligations-document/html-builder.ts +++ b/admin-compliance/lib/sdk/obligations-document/html-builder.ts @@ -2,8 +2,7 @@ // 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 { Obligation, ObligationComplianceCheckResult } from '../obligations-compliance' import type { ObligationDocumentOrgHeader, ObligationDocumentRevision } from './types-defaults' import { STATUS_LABELS_DE, @@ -11,8 +10,17 @@ import { PRIORITY_LABELS_DE, PRIORITY_BADGE_CLASSES, } from './types-defaults' -import { escHtml, formatDateDE, daysBetween } from './helpers' +import { escHtml, formatDateDE } from './helpers' import { getDocumentStyles } from './html-styles' +import { + buildSection6, + buildSection7, + buildSection8, + buildSection9, + buildSection10, + buildSection11, + buildFooter, +} from './html-builder-sections-6-11' export function buildObligationDocumentHtml( obligations: Obligation[], @@ -294,327 +302,3 @@ function buildSection5(bySource: Map): string { 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 ` - - - -` -}