refactor: split oversized html-builder files under 500 LOC hard cap
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>
This commit is contained in:
@@ -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, Obligation[]>): string {
|
||||
const priorityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 }
|
||||
|
||||
let html = `
|
||||
<div class="section">
|
||||
<div class="section-header">6. Detaillierte Pflichten</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
|
||||
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 += ` <h3 style="color: #5b21b6; margin: 20px 0 10px 0; font-size: 11pt;">${escHtml(source)} <span style="font-weight: 400; font-size: 9pt; color: #64748b;">(${sorted.length} Pflichten)</span></h3>
|
||||
`
|
||||
|
||||
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(', ')
|
||||
: '<em style="color: #d97706;">Kein Nachweis</em>'
|
||||
const systemsStr = o.linked_systems && o.linked_systems.length > 0
|
||||
? o.linked_systems.map(s => escHtml(s)).join(', ')
|
||||
: '—'
|
||||
|
||||
html += `
|
||||
<div class="policy-detail">
|
||||
<div class="policy-detail-header">
|
||||
<span>${escHtml(o.title)}</span>
|
||||
<span class="badge ${statusBadge}">${escHtml(statusLabel)}</span>
|
||||
</div>
|
||||
<div class="policy-detail-body">
|
||||
<table>
|
||||
<tr><th>Rechtsquelle</th><td>${escHtml(o.source)} ${escHtml(o.source_article || '')}</td></tr>
|
||||
<tr><th>Beschreibung</th><td>${escHtml(o.description || '—')}</td></tr>
|
||||
<tr><th>Prioritaet</th><td><span class="badge ${priorityBadge}">${escHtml(priorityLabel)}</span></td></tr>
|
||||
<tr><th>Status</th><td><span class="badge ${statusBadge}">${escHtml(statusLabel)}</span></td></tr>
|
||||
<tr><th>Verantwortlich</th><td>${escHtml(o.responsible || '—')}</td></tr>
|
||||
<tr><th>Frist</th><td>${deadlineStr}</td></tr>
|
||||
<tr><th>Nachweise</th><td>${evidenceStr}</td></tr>
|
||||
<tr><th>Betroffene Systeme</th><td>${systemsStr}</td></tr>
|
||||
${o.linked_vendor_ids && o.linked_vendor_ids.length > 0 ? `<tr><th>Auftragsverarbeiter</th><td>${o.linked_vendor_ids.map(id => escHtml(id)).join(', ')}</td></tr>` : ''}
|
||||
${o.notes ? `<tr><th>Notizen</th><td>${escHtml(o.notes)}</td></tr>` : ''}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
return html
|
||||
}
|
||||
|
||||
export function buildSection7(roleMap: Map<string, Obligation[]>): string {
|
||||
let html = `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">7. Verantwortlichkeiten</div>
|
||||
<div class="section-body">
|
||||
<p>Die folgende Rollenmatrix zeigt, welche Personen oder Abteilungen fuer welche Pflichten
|
||||
die Umsetzungsverantwortung tragen:</p>
|
||||
<table>
|
||||
<tr><th>Verantwortlich</th><th>Pflichten</th><th>Anzahl</th><th>Davon offen</th></tr>
|
||||
`
|
||||
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 += ` <tr>
|
||||
<td>${escHtml(role)}</td>
|
||||
<td>${titles.join('; ')}${suffix}</td>
|
||||
<td>${obls.length}</td>
|
||||
<td>${openCount}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
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 = `
|
||||
<div class="section">
|
||||
<div class="section-header">8. Fristen und Termine</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
if (overdue.length > 0) {
|
||||
html += ` <h4 style="color: #dc2626; margin-bottom: 8px;">Ueberfaellige Pflichten (${overdue.length})</h4>
|
||||
<table>
|
||||
<tr><th>Pflicht</th><th>Regulierung</th><th>Frist</th><th>Tage ueberfaellig</th><th>Prioritaet</th></tr>
|
||||
`
|
||||
for (const o of overdue) {
|
||||
const days = daysBetween(new Date(o.deadline!), now)
|
||||
html += ` <tr>
|
||||
<td>${escHtml(o.title)}</td>
|
||||
<td>${escHtml(o.source)}</td>
|
||||
<td>${formatDateDE(o.deadline)}</td>
|
||||
<td><span class="badge badge-critical">${days} Tage</span></td>
|
||||
<td><span class="badge ${PRIORITY_BADGE_CLASSES[o.priority] || 'badge-draft'}">${escHtml(PRIORITY_LABELS_DE[o.priority] || o.priority)}</span></td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
}
|
||||
|
||||
if (upcoming.length > 0) {
|
||||
html += ` <h4 style="color: #5b21b6; margin: 16px 0 8px 0;">Anstehende Fristen (${upcoming.length})</h4>
|
||||
<table>
|
||||
<tr><th>Pflicht</th><th>Regulierung</th><th>Frist</th><th>Verbleibend</th><th>Verantwortlich</th></tr>
|
||||
`
|
||||
for (const o of upcoming.slice(0, 20)) {
|
||||
const days = daysBetween(now, new Date(o.deadline!))
|
||||
html += ` <tr>
|
||||
<td>${escHtml(o.title)}</td>
|
||||
<td>${escHtml(o.source)}</td>
|
||||
<td>${formatDateDE(o.deadline)}</td>
|
||||
<td>${days} Tage</td>
|
||||
<td>${escHtml(o.responsible || '—')}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
if (upcoming.length > 20) {
|
||||
html += ` <tr><td colspan="5" style="text-align: center; color: #64748b;">... und ${upcoming.length - 20} weitere</td></tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
}
|
||||
|
||||
if (withDeadline.length === 0) {
|
||||
html += ` <p><em>Keine offenen Pflichten mit Fristen vorhanden.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
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 = `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">9. Nachweisverzeichnis</div>
|
||||
<div class="section-body">
|
||||
<p>${withEvidence.length} von ${obligations.length} Pflichten haben Nachweise hinterlegt.</p>
|
||||
`
|
||||
if (withEvidence.length > 0) {
|
||||
html += ` <table>
|
||||
<tr><th>Pflicht</th><th>Regulierung</th><th>Nachweise</th><th>Status</th></tr>
|
||||
`
|
||||
for (const o of withEvidence) {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(o.title)}</td>
|
||||
<td>${escHtml(o.source)}</td>
|
||||
<td>${o.evidence!.map(e => escHtml(e)).join(', ')}</td>
|
||||
<td><span class="badge ${STATUS_BADGE_CLASSES[o.status] || 'badge-draft'}">${escHtml(STATUS_LABELS_DE[o.status] || o.status)}</span></td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
}
|
||||
|
||||
if (withoutEvidence.length > 0) {
|
||||
html += ` <p style="margin-top: 12px;"><strong>Pflichten ohne Nachweise (${withoutEvidence.length}):</strong></p>
|
||||
<ul style="margin: 4px 0 8px 24px; font-size: 9pt; color: #d97706;">
|
||||
`
|
||||
for (const o of withoutEvidence.slice(0, 15)) {
|
||||
html += ` <li>${escHtml(o.title)} (${escHtml(o.source)})</li>
|
||||
`
|
||||
}
|
||||
if (withoutEvidence.length > 15) {
|
||||
html += ` <li>... und ${withoutEvidence.length - 15} weitere</li>
|
||||
`
|
||||
}
|
||||
html += ` </ul>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
return html
|
||||
}
|
||||
|
||||
export function buildSection10(complianceResult: ObligationComplianceCheckResult | null): string {
|
||||
let html = `
|
||||
<div class="section">
|
||||
<div class="section-header">10. 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>Geprueft am</td><td>${formatDateDE(complianceResult.checkedAt)}</td></tr>
|
||||
<tr><td>Befunde gesamt</td><td>${complianceResult.summary.total}</td></tr>
|
||||
<tr><td>Kritisch</td><td>${complianceResult.summary.critical}</td></tr>
|
||||
<tr><td>Hoch</td><td>${complianceResult.summary.high}</td></tr>
|
||||
<tr><td>Mittel</td><td>${complianceResult.summary.medium}</td></tr>
|
||||
<tr><td>Niedrig</td><td>${complianceResult.summary.low}</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>Befund</th><th>Betroffene Pflichten</th><th>Empfehlung</th></tr>
|
||||
`
|
||||
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 += ` <tr>
|
||||
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${OBLIGATION_SEVERITY_COLORS[sev]}">${OBLIGATION_SEVERITY_LABELS_DE[sev]}</span></td>
|
||||
<td>${escHtml(issue.message)}</td>
|
||||
<td>${issue.affectedObligations.length > 0 ? issue.affectedObligations.length + ' Pflicht(en)' : '—'}</td>
|
||||
<td>${escHtml(issue.recommendation)}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
} else {
|
||||
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Pflichten sind konform.</em></p>
|
||||
`
|
||||
}
|
||||
} else {
|
||||
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im
|
||||
Pflichtenregister-Tab durch, um den Status in das Dokument aufzunehmen.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
return html
|
||||
}
|
||||
|
||||
export function buildSection11(
|
||||
revisions: ObligationDocumentRevision[],
|
||||
orgHeader: ObligationDocumentOrgHeader,
|
||||
today: string
|
||||
): string {
|
||||
let html = `
|
||||
<div class="section">
|
||||
<div class="section-header">11. 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.documentVersion)}</td>
|
||||
<td>${today}</td>
|
||||
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '—')}</td>
|
||||
<td>Erstversion des Pflichtenregisters</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
return html
|
||||
}
|
||||
|
||||
export function buildFooter(orgName: string, today: string, version: string): string {
|
||||
return `
|
||||
<div class="page-footer">
|
||||
<span>Pflichtenregister — ${escHtml(orgName)}</span>
|
||||
<span>Stand: ${today} | Version ${escHtml(version)}</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
@@ -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, Obligation[]>): string {
|
||||
return html
|
||||
}
|
||||
|
||||
function buildSection6(bySource: Map<string, Obligation[]>): string {
|
||||
const priorityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 }
|
||||
|
||||
let html = `
|
||||
<div class="section">
|
||||
<div class="section-header">6. Detaillierte Pflichten</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
|
||||
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 += ` <h3 style="color: #5b21b6; margin: 20px 0 10px 0; font-size: 11pt;">${escHtml(source)} <span style="font-weight: 400; font-size: 9pt; color: #64748b;">(${sorted.length} Pflichten)</span></h3>
|
||||
`
|
||||
|
||||
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(', ')
|
||||
: '<em style="color: #d97706;">Kein Nachweis</em>'
|
||||
const systemsStr = o.linked_systems && o.linked_systems.length > 0
|
||||
? o.linked_systems.map(s => escHtml(s)).join(', ')
|
||||
: '—'
|
||||
|
||||
html += `
|
||||
<div class="policy-detail">
|
||||
<div class="policy-detail-header">
|
||||
<span>${escHtml(o.title)}</span>
|
||||
<span class="badge ${statusBadge}">${escHtml(statusLabel)}</span>
|
||||
</div>
|
||||
<div class="policy-detail-body">
|
||||
<table>
|
||||
<tr><th>Rechtsquelle</th><td>${escHtml(o.source)} ${escHtml(o.source_article || '')}</td></tr>
|
||||
<tr><th>Beschreibung</th><td>${escHtml(o.description || '—')}</td></tr>
|
||||
<tr><th>Prioritaet</th><td><span class="badge ${priorityBadge}">${escHtml(priorityLabel)}</span></td></tr>
|
||||
<tr><th>Status</th><td><span class="badge ${statusBadge}">${escHtml(statusLabel)}</span></td></tr>
|
||||
<tr><th>Verantwortlich</th><td>${escHtml(o.responsible || '—')}</td></tr>
|
||||
<tr><th>Frist</th><td>${deadlineStr}</td></tr>
|
||||
<tr><th>Nachweise</th><td>${evidenceStr}</td></tr>
|
||||
<tr><th>Betroffene Systeme</th><td>${systemsStr}</td></tr>
|
||||
${o.linked_vendor_ids && o.linked_vendor_ids.length > 0 ? `<tr><th>Auftragsverarbeiter</th><td>${o.linked_vendor_ids.map(id => escHtml(id)).join(', ')}</td></tr>` : ''}
|
||||
${o.notes ? `<tr><th>Notizen</th><td>${escHtml(o.notes)}</td></tr>` : ''}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
return html
|
||||
}
|
||||
|
||||
function buildSection7(roleMap: Map<string, Obligation[]>): string {
|
||||
let html = `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">7. Verantwortlichkeiten</div>
|
||||
<div class="section-body">
|
||||
<p>Die folgende Rollenmatrix zeigt, welche Personen oder Abteilungen fuer welche Pflichten
|
||||
die Umsetzungsverantwortung tragen:</p>
|
||||
<table>
|
||||
<tr><th>Verantwortlich</th><th>Pflichten</th><th>Anzahl</th><th>Davon offen</th></tr>
|
||||
`
|
||||
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 += ` <tr>
|
||||
<td>${escHtml(role)}</td>
|
||||
<td>${titles.join('; ')}${suffix}</td>
|
||||
<td>${obls.length}</td>
|
||||
<td>${openCount}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
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 = `
|
||||
<div class="section">
|
||||
<div class="section-header">8. Fristen und Termine</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
if (overdue.length > 0) {
|
||||
html += ` <h4 style="color: #dc2626; margin-bottom: 8px;">Ueberfaellige Pflichten (${overdue.length})</h4>
|
||||
<table>
|
||||
<tr><th>Pflicht</th><th>Regulierung</th><th>Frist</th><th>Tage ueberfaellig</th><th>Prioritaet</th></tr>
|
||||
`
|
||||
for (const o of overdue) {
|
||||
const days = daysBetween(new Date(o.deadline!), now)
|
||||
html += ` <tr>
|
||||
<td>${escHtml(o.title)}</td>
|
||||
<td>${escHtml(o.source)}</td>
|
||||
<td>${formatDateDE(o.deadline)}</td>
|
||||
<td><span class="badge badge-critical">${days} Tage</span></td>
|
||||
<td><span class="badge ${PRIORITY_BADGE_CLASSES[o.priority] || 'badge-draft'}">${escHtml(PRIORITY_LABELS_DE[o.priority] || o.priority)}</span></td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
}
|
||||
|
||||
if (upcoming.length > 0) {
|
||||
html += ` <h4 style="color: #5b21b6; margin: 16px 0 8px 0;">Anstehende Fristen (${upcoming.length})</h4>
|
||||
<table>
|
||||
<tr><th>Pflicht</th><th>Regulierung</th><th>Frist</th><th>Verbleibend</th><th>Verantwortlich</th></tr>
|
||||
`
|
||||
for (const o of upcoming.slice(0, 20)) {
|
||||
const days = daysBetween(now, new Date(o.deadline!))
|
||||
html += ` <tr>
|
||||
<td>${escHtml(o.title)}</td>
|
||||
<td>${escHtml(o.source)}</td>
|
||||
<td>${formatDateDE(o.deadline)}</td>
|
||||
<td>${days} Tage</td>
|
||||
<td>${escHtml(o.responsible || '—')}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
if (upcoming.length > 20) {
|
||||
html += ` <tr><td colspan="5" style="text-align: center; color: #64748b;">... und ${upcoming.length - 20} weitere</td></tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
}
|
||||
|
||||
if (withDeadline.length === 0) {
|
||||
html += ` <p><em>Keine offenen Pflichten mit Fristen vorhanden.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
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 = `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">9. Nachweisverzeichnis</div>
|
||||
<div class="section-body">
|
||||
<p>${withEvidence.length} von ${obligations.length} Pflichten haben Nachweise hinterlegt.</p>
|
||||
`
|
||||
if (withEvidence.length > 0) {
|
||||
html += ` <table>
|
||||
<tr><th>Pflicht</th><th>Regulierung</th><th>Nachweise</th><th>Status</th></tr>
|
||||
`
|
||||
for (const o of withEvidence) {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(o.title)}</td>
|
||||
<td>${escHtml(o.source)}</td>
|
||||
<td>${o.evidence!.map(e => escHtml(e)).join(', ')}</td>
|
||||
<td><span class="badge ${STATUS_BADGE_CLASSES[o.status] || 'badge-draft'}">${escHtml(STATUS_LABELS_DE[o.status] || o.status)}</span></td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
}
|
||||
|
||||
if (withoutEvidence.length > 0) {
|
||||
html += ` <p style="margin-top: 12px;"><strong>Pflichten ohne Nachweise (${withoutEvidence.length}):</strong></p>
|
||||
<ul style="margin: 4px 0 8px 24px; font-size: 9pt; color: #d97706;">
|
||||
`
|
||||
for (const o of withoutEvidence.slice(0, 15)) {
|
||||
html += ` <li>${escHtml(o.title)} (${escHtml(o.source)})</li>
|
||||
`
|
||||
}
|
||||
if (withoutEvidence.length > 15) {
|
||||
html += ` <li>... und ${withoutEvidence.length - 15} weitere</li>
|
||||
`
|
||||
}
|
||||
html += ` </ul>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
return html
|
||||
}
|
||||
|
||||
function buildSection10(complianceResult: ObligationComplianceCheckResult | null): string {
|
||||
let html = `
|
||||
<div class="section">
|
||||
<div class="section-header">10. 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>Geprueft am</td><td>${formatDateDE(complianceResult.checkedAt)}</td></tr>
|
||||
<tr><td>Befunde gesamt</td><td>${complianceResult.summary.total}</td></tr>
|
||||
<tr><td>Kritisch</td><td>${complianceResult.summary.critical}</td></tr>
|
||||
<tr><td>Hoch</td><td>${complianceResult.summary.high}</td></tr>
|
||||
<tr><td>Mittel</td><td>${complianceResult.summary.medium}</td></tr>
|
||||
<tr><td>Niedrig</td><td>${complianceResult.summary.low}</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>Befund</th><th>Betroffene Pflichten</th><th>Empfehlung</th></tr>
|
||||
`
|
||||
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 += ` <tr>
|
||||
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${OBLIGATION_SEVERITY_COLORS[sev]}">${OBLIGATION_SEVERITY_LABELS_DE[sev]}</span></td>
|
||||
<td>${escHtml(issue.message)}</td>
|
||||
<td>${issue.affectedObligations.length > 0 ? issue.affectedObligations.length + ' Pflicht(en)' : '—'}</td>
|
||||
<td>${escHtml(issue.recommendation)}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
} else {
|
||||
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Pflichten sind konform.</em></p>
|
||||
`
|
||||
}
|
||||
} else {
|
||||
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im
|
||||
Pflichtenregister-Tab durch, um den Status in das Dokument aufzunehmen.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
return html
|
||||
}
|
||||
|
||||
function buildSection11(
|
||||
revisions: ObligationDocumentRevision[],
|
||||
orgHeader: ObligationDocumentOrgHeader,
|
||||
today: string
|
||||
): string {
|
||||
let html = `
|
||||
<div class="section">
|
||||
<div class="section-header">11. 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.documentVersion)}</td>
|
||||
<td>${today}</td>
|
||||
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '—')}</td>
|
||||
<td>Erstversion des Pflichtenregisters</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
return html
|
||||
}
|
||||
|
||||
function buildFooter(orgName: string, today: string, version: string): string {
|
||||
return `
|
||||
<div class="page-footer">
|
||||
<span>Pflichtenregister — ${escHtml(orgName)}</span>
|
||||
<span>Stand: ${today} | Version ${escHtml(version)}</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user