[split-required] Split 58 monoliths across Python, Go, TypeScript (Phases 1-3)
Phase 1 — Python (klausur-service): 5 monoliths → 36 files - dsfa_corpus_ingestion.py (1,828 LOC → 5 files) - cv_ocr_engines.py (2,102 LOC → 7 files) - cv_layout.py (3,653 LOC → 10 files) - vocab_worksheet_api.py (2,783 LOC → 8 files) - grid_build_core.py (1,958 LOC → 6 files) Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files - staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3) - policy_handlers.go (700 → 2), repository.go (684 → 2) - search.go (592 → 2), ai_extraction_handlers.go (554 → 2) - seed_data.go (591 → 2), grade_service.go (646 → 2) Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files - sdk/types.ts (2,108 → 16 domain files) - ai/rag/page.tsx (2,686 → 14 files) - 22 page.tsx files split into _components/ + _hooks/ - 11 component files split into sub-components - 10 SDK data catalogs added to loc-exceptions - Deleted dead backup index_original.ts (4,899 LOC) All original public APIs preserved via re-export facades. Zero new errors: Python imports verified, Go builds clean, TypeScript tsc --noEmit shows only pre-existing errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
312
admin-lehrer/lib/sdk/export-pdf.ts
Normal file
312
admin-lehrer/lib/sdk/export-pdf.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
/**
|
||||
* PDF Export
|
||||
* Generates a multi-page compliance report as PDF
|
||||
*/
|
||||
|
||||
import jsPDF from 'jspdf'
|
||||
import { SDKState, SDK_STEPS } from './types'
|
||||
import { ExportOptions, DEFAULT_OPTIONS, LABELS_DE } from './export-types'
|
||||
import {
|
||||
formatDate,
|
||||
addHeader,
|
||||
addFooter,
|
||||
addSectionTitle,
|
||||
addSubsectionTitle,
|
||||
addText,
|
||||
checkPageBreak,
|
||||
} from './export-pdf-helpers'
|
||||
|
||||
export async function exportToPDF(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
const doc = new jsPDF()
|
||||
|
||||
let y = 30
|
||||
const pageWidth = doc.internal.pageSize.getWidth()
|
||||
|
||||
// ==========================================================================
|
||||
// Title Page
|
||||
// ==========================================================================
|
||||
|
||||
// Logo/Title area
|
||||
doc.setFillColor(147, 51, 234)
|
||||
doc.rect(0, 0, pageWidth, 60, 'F')
|
||||
|
||||
doc.setFontSize(24)
|
||||
doc.setTextColor(255)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(LABELS_DE.title, 20, 35)
|
||||
|
||||
doc.setFontSize(14)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.text(LABELS_DE.subtitle, 20, 48)
|
||||
|
||||
// Reset for content
|
||||
y = 80
|
||||
|
||||
// Summary box
|
||||
doc.setDrawColor(200)
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y, pageWidth - 40, 50, 3, 3, 'FD')
|
||||
|
||||
y += 15
|
||||
doc.setFontSize(12)
|
||||
doc.setTextColor(60)
|
||||
doc.text(`${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 30, y)
|
||||
|
||||
y += 10
|
||||
doc.text(`Tenant ID: ${state.tenantId}`, 30, y)
|
||||
|
||||
y += 10
|
||||
doc.text(`Version: ${state.version}`, 30, y)
|
||||
|
||||
y += 10
|
||||
const completedSteps = state.completedSteps.length
|
||||
const totalSteps = SDK_STEPS.length
|
||||
doc.text(`${LABELS_DE.progress}: ${completedSteps}/${totalSteps} Schritte (${Math.round(completedSteps / totalSteps * 100)}%)`, 30, y)
|
||||
|
||||
y += 30
|
||||
|
||||
// Table of Contents
|
||||
y = addSectionTitle(doc, 'Inhaltsverzeichnis', y)
|
||||
|
||||
const tocItems = [
|
||||
{ title: 'Zusammenfassung', page: 2 },
|
||||
{ title: 'Phase 1: Compliance Assessment', page: 3 },
|
||||
{ title: 'Phase 2: Dokumentengenerierung', page: 4 },
|
||||
{ title: 'Risiken & Controls', page: 5 },
|
||||
{ title: 'Checkpoints', page: 6 },
|
||||
]
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(80)
|
||||
tocItems.forEach((item, idx) => {
|
||||
doc.text(`${idx + 1}. ${item.title}`, 25, y)
|
||||
doc.text(`${item.page}`, pageWidth - 30, y, { align: 'right' })
|
||||
y += 7
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// Summary Page
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
|
||||
y = addSectionTitle(doc, LABELS_DE.summary, y)
|
||||
|
||||
// Progress overview
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y, pageWidth - 40, 40, 3, 3, 'F')
|
||||
|
||||
y += 15
|
||||
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1)
|
||||
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2)
|
||||
const phase1Completed = phase1Steps.filter(s => state.completedSteps.includes(s.id)).length
|
||||
const phase2Completed = phase2Steps.filter(s => state.completedSteps.includes(s.id)).length
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(60)
|
||||
doc.text(`${LABELS_DE.phase1}: ${phase1Completed}/${phase1Steps.length} ${LABELS_DE.completed}`, 30, y)
|
||||
y += 8
|
||||
doc.text(`${LABELS_DE.phase2}: ${phase2Completed}/${phase2Steps.length} ${LABELS_DE.completed}`, 30, y)
|
||||
|
||||
y += 25
|
||||
|
||||
// Key metrics
|
||||
y = addSubsectionTitle(doc, 'Kennzahlen', y)
|
||||
|
||||
const metrics = [
|
||||
{ label: 'Use Cases', value: state.useCases.length },
|
||||
{ label: 'Risiken identifiziert', value: state.risks.length },
|
||||
{ label: 'Controls definiert', value: state.controls.length },
|
||||
{ label: 'Anforderungen', value: state.requirements.length },
|
||||
{ label: 'Nachweise', value: state.evidence.length },
|
||||
]
|
||||
|
||||
metrics.forEach(metric => {
|
||||
doc.text(`${metric.label}: ${metric.value}`, 30, y)
|
||||
y += 7
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// Use Cases
|
||||
// ==========================================================================
|
||||
|
||||
y += 10
|
||||
y = checkPageBreak(doc, y)
|
||||
y = addSectionTitle(doc, LABELS_DE.useCases, y)
|
||||
|
||||
if (state.useCases.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
state.useCases.forEach((uc, idx) => {
|
||||
y = checkPageBreak(doc, y, 50)
|
||||
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y - 5, pageWidth - 40, 35, 2, 2, 'F')
|
||||
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${uc.name}`, 25, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
const ucStatus = uc.stepsCompleted === uc.steps.length ? LABELS_DE.completed : `${uc.stepsCompleted}/${uc.steps.length} Schritte`
|
||||
doc.text(`ID: ${uc.id} | ${LABELS_DE.status}: ${ucStatus}`, 25, y + 13)
|
||||
|
||||
if (uc.description) {
|
||||
y = addText(doc, uc.description, 25, y + 21, 160)
|
||||
}
|
||||
|
||||
y += 40
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Risks
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.risks, y)
|
||||
|
||||
if (state.risks.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
// Sort by severity
|
||||
const sortedRisks = [...state.risks].sort((a, b) => {
|
||||
const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
||||
return (order[a.severity] || 4) - (order[b.severity] || 4)
|
||||
})
|
||||
|
||||
sortedRisks.forEach((risk, idx) => {
|
||||
y = checkPageBreak(doc, y, 45)
|
||||
|
||||
// Severity color
|
||||
const severityColors: Record<string, [number, number, number]> = {
|
||||
CRITICAL: [220, 38, 38],
|
||||
HIGH: [234, 88, 12],
|
||||
MEDIUM: [234, 179, 8],
|
||||
LOW: [34, 197, 94],
|
||||
}
|
||||
const color = severityColors[risk.severity] || [100, 100, 100]
|
||||
|
||||
doc.setFillColor(color[0], color[1], color[2])
|
||||
doc.rect(20, y - 3, 3, 30, 'F')
|
||||
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${risk.title}`, 28, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.severity}: ${risk.severity} | ${LABELS_DE.category}: ${risk.category}`, 28, y + 13)
|
||||
|
||||
if (risk.description) {
|
||||
y = addText(doc, risk.description, 28, y + 21, 155)
|
||||
}
|
||||
|
||||
if (risk.mitigation && risk.mitigation.length > 0) {
|
||||
y += 5
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(34, 197, 94)
|
||||
doc.text(`${LABELS_DE.mitigation}: ${risk.mitigation.join(', ')}`, 28, y)
|
||||
}
|
||||
|
||||
y += 15
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Controls
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.controls, y)
|
||||
|
||||
if (state.controls.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
state.controls.forEach((ctrl, idx) => {
|
||||
y = checkPageBreak(doc, y, 35)
|
||||
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y - 5, pageWidth - 40, 28, 2, 2, 'F')
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${ctrl.name}`, 25, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.category}: ${ctrl.category} | ${LABELS_DE.implementation}: ${ctrl.implementationStatus || 'Nicht definiert'}`, 25, y + 13)
|
||||
|
||||
if (ctrl.description) {
|
||||
y = addText(doc, ctrl.description.substring(0, 150) + (ctrl.description.length > 150 ? '...' : ''), 25, y + 20, 160)
|
||||
}
|
||||
|
||||
y += 35
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Checkpoints
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.checkpoints, y)
|
||||
|
||||
const checkpointIds = Object.keys(state.checkpoints)
|
||||
|
||||
if (checkpointIds.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
checkpointIds.forEach((cpId) => {
|
||||
const cp = state.checkpoints[cpId]
|
||||
y = checkPageBreak(doc, y, 25)
|
||||
|
||||
const statusColor = cp.passed ? [34, 197, 94] : [220, 38, 38]
|
||||
doc.setFillColor(statusColor[0], statusColor[1], statusColor[2])
|
||||
doc.circle(25, y + 2, 3, 'F')
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(40)
|
||||
doc.text(cpId, 35, y + 5)
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.status}: ${cp.passed ? LABELS_DE.completed : LABELS_DE.pending}`, 35, y + 12)
|
||||
|
||||
if (cp.errors && cp.errors.length > 0) {
|
||||
doc.setTextColor(220, 38, 38)
|
||||
doc.text(`Fehler: ${cp.errors.map(e => e.message).join(', ')}`, 35, y + 19)
|
||||
y += 7
|
||||
}
|
||||
|
||||
y += 20
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Add page numbers
|
||||
// ==========================================================================
|
||||
|
||||
const pageCount = doc.getNumberOfPages()
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i)
|
||||
if (i > 1) {
|
||||
addHeader(doc, LABELS_DE.title, i, pageCount)
|
||||
}
|
||||
addFooter(doc, state)
|
||||
}
|
||||
|
||||
return doc.output('blob')
|
||||
}
|
||||
Reference in New Issue
Block a user