All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
Phase 1: Fix completeness gates G23 (require verified/rejected mitigations) and G09 (audit trail check) Phase 2: LLM-based tech-file section generation with 19 German prompts and RAG enrichment Phase 3: Multi-format document export (PDF/Excel/DOCX/Markdown/JSON) Phase 4: Company profile → IACE data flow with auto component/classification creation Phase 5: TipTap WYSIWYG editor replacing textarea for tech-file sections Phase 6: User journey tests, developer portal API reference, updated documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1102 lines
34 KiB
Go
1102 lines
34 KiB
Go
package iace
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jung-kurt/gofpdf"
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
// ExportFormat represents a supported document export format
|
|
type ExportFormat string
|
|
|
|
const (
|
|
ExportFormatPDF ExportFormat = "pdf"
|
|
ExportFormatXLSX ExportFormat = "xlsx"
|
|
ExportFormatDOCX ExportFormat = "docx"
|
|
ExportFormatMD ExportFormat = "md"
|
|
ExportFormatJSON ExportFormat = "json"
|
|
)
|
|
|
|
// DocumentExporter handles exporting CE technical file data into various formats
|
|
type DocumentExporter struct{}
|
|
|
|
// NewDocumentExporter creates a new DocumentExporter instance
|
|
func NewDocumentExporter() *DocumentExporter {
|
|
return &DocumentExporter{}
|
|
}
|
|
|
|
// ============================================================================
|
|
// PDF Export
|
|
// ============================================================================
|
|
|
|
// ExportPDF generates a PDF document containing the full CE technical file
|
|
func (e *DocumentExporter) ExportPDF(
|
|
project *Project,
|
|
sections []TechFileSection,
|
|
hazards []Hazard,
|
|
assessments []RiskAssessment,
|
|
mitigations []Mitigation,
|
|
classifications []RegulatoryClassification,
|
|
) ([]byte, error) {
|
|
if project == nil {
|
|
return nil, fmt.Errorf("project must not be nil")
|
|
}
|
|
|
|
pdf := gofpdf.New("P", "mm", "A4", "")
|
|
pdf.SetFont("Helvetica", "", 12)
|
|
|
|
// --- Cover Page ---
|
|
pdf.AddPage()
|
|
e.pdfCoverPage(pdf, project)
|
|
|
|
// --- Table of Contents ---
|
|
pdf.AddPage()
|
|
e.pdfTableOfContents(pdf, sections)
|
|
|
|
// --- Sections ---
|
|
for _, section := range sections {
|
|
pdf.AddPage()
|
|
e.pdfSection(pdf, section)
|
|
}
|
|
|
|
// --- Hazard Log ---
|
|
pdf.AddPage()
|
|
e.pdfHazardLog(pdf, hazards, assessments)
|
|
|
|
// --- Risk Matrix Summary ---
|
|
e.pdfRiskMatrixSummary(pdf, assessments)
|
|
|
|
// --- Mitigations Table ---
|
|
pdf.AddPage()
|
|
e.pdfMitigationsTable(pdf, mitigations)
|
|
|
|
// --- Regulatory Classifications ---
|
|
if len(classifications) > 0 {
|
|
pdf.AddPage()
|
|
e.pdfClassifications(pdf, classifications)
|
|
}
|
|
|
|
// --- Footer on every page ---
|
|
pdf.SetFooterFunc(func() {
|
|
pdf.SetY(-15)
|
|
pdf.SetFont("Helvetica", "I", 8)
|
|
pdf.SetTextColor(128, 128, 128)
|
|
pdf.CellFormat(0, 5,
|
|
fmt.Sprintf("CE-Akte %s | Generiert am %s | BreakPilot AI Compliance SDK",
|
|
project.MachineName, time.Now().Format("02.01.2006 15:04")),
|
|
"", 0, "C", false, 0, "")
|
|
})
|
|
|
|
var buf bytes.Buffer
|
|
if err := pdf.Output(&buf); err != nil {
|
|
return nil, fmt.Errorf("failed to generate PDF: %w", err)
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func (e *DocumentExporter) pdfCoverPage(pdf *gofpdf.Fpdf, project *Project) {
|
|
pdf.Ln(60)
|
|
|
|
// Machine name (large, bold, centered)
|
|
pdf.SetFont("Helvetica", "B", 28)
|
|
pdf.SetTextColor(0, 0, 0)
|
|
pdf.CellFormat(0, 15, "CE-Technische Akte", "", 1, "C", false, 0, "")
|
|
pdf.Ln(5)
|
|
|
|
pdf.SetFont("Helvetica", "B", 22)
|
|
pdf.CellFormat(0, 12, project.MachineName, "", 1, "C", false, 0, "")
|
|
pdf.Ln(15)
|
|
|
|
// Metadata block
|
|
pdf.SetFont("Helvetica", "", 12)
|
|
coverItems := []struct {
|
|
label string
|
|
value string
|
|
}{
|
|
{"Hersteller", project.Manufacturer},
|
|
{"Maschinentyp", project.MachineType},
|
|
{"CE-Kennzeichnungsziel", project.CEMarkingTarget},
|
|
{"Projektstatus", string(project.Status)},
|
|
{"Datum", time.Now().Format("02.01.2006")},
|
|
}
|
|
|
|
for _, item := range coverItems {
|
|
if item.value == "" {
|
|
continue
|
|
}
|
|
pdf.SetFont("Helvetica", "B", 12)
|
|
pdf.CellFormat(60, 8, item.label+":", "", 0, "R", false, 0, "")
|
|
pdf.SetFont("Helvetica", "", 12)
|
|
pdf.CellFormat(5, 8, "", "", 0, "", false, 0, "")
|
|
pdf.CellFormat(0, 8, item.value, "", 1, "L", false, 0, "")
|
|
}
|
|
|
|
if project.Description != "" {
|
|
pdf.Ln(15)
|
|
pdf.SetFont("Helvetica", "I", 10)
|
|
pdf.MultiCell(0, 5, project.Description, "", "C", false)
|
|
}
|
|
}
|
|
|
|
func (e *DocumentExporter) pdfTableOfContents(pdf *gofpdf.Fpdf, sections []TechFileSection) {
|
|
pdf.SetFont("Helvetica", "B", 16)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.CellFormat(0, 10, "Inhaltsverzeichnis", "", 1, "L", false, 0, "")
|
|
pdf.SetTextColor(0, 0, 0)
|
|
pdf.SetDrawColor(200, 200, 200)
|
|
pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
|
|
pdf.Ln(8)
|
|
|
|
pdf.SetFont("Helvetica", "", 11)
|
|
|
|
// Fixed sections first
|
|
fixedEntries := []string{
|
|
"Gefaehrdungsprotokoll",
|
|
"Risikomatrix-Zusammenfassung",
|
|
"Massnahmen-Uebersicht",
|
|
}
|
|
|
|
pageEstimate := 3 // Cover + TOC use pages 1-2, sections start at 3
|
|
for i, section := range sections {
|
|
pdf.CellFormat(10, 7, fmt.Sprintf("%d.", i+1), "", 0, "R", false, 0, "")
|
|
pdf.CellFormat(5, 7, "", "", 0, "", false, 0, "")
|
|
pdf.CellFormat(130, 7, section.Title, "", 0, "L", false, 0, "")
|
|
pdf.CellFormat(0, 7, fmt.Sprintf("~%d", pageEstimate+i), "", 1, "R", false, 0, "")
|
|
}
|
|
|
|
// Append fixed sections after document sections
|
|
startPage := pageEstimate + len(sections)
|
|
for i, entry := range fixedEntries {
|
|
idx := len(sections) + i + 1
|
|
pdf.CellFormat(10, 7, fmt.Sprintf("%d.", idx), "", 0, "R", false, 0, "")
|
|
pdf.CellFormat(5, 7, "", "", 0, "", false, 0, "")
|
|
pdf.CellFormat(130, 7, entry, "", 0, "L", false, 0, "")
|
|
pdf.CellFormat(0, 7, fmt.Sprintf("~%d", startPage+i), "", 1, "R", false, 0, "")
|
|
}
|
|
}
|
|
|
|
func (e *DocumentExporter) pdfSection(pdf *gofpdf.Fpdf, section TechFileSection) {
|
|
// Section heading
|
|
pdf.SetFont("Helvetica", "B", 14)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.CellFormat(0, 10, section.Title, "", 1, "L", false, 0, "")
|
|
pdf.SetTextColor(0, 0, 0)
|
|
|
|
// Status badge
|
|
pdf.SetFont("Helvetica", "I", 9)
|
|
pdf.SetTextColor(100, 100, 100)
|
|
pdf.CellFormat(0, 5,
|
|
fmt.Sprintf("Typ: %s | Status: %s | Version: %d",
|
|
section.SectionType, string(section.Status), section.Version),
|
|
"", 1, "L", false, 0, "")
|
|
pdf.SetTextColor(0, 0, 0)
|
|
|
|
pdf.SetDrawColor(200, 200, 200)
|
|
pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
|
|
pdf.Ln(5)
|
|
|
|
// Content
|
|
pdf.SetFont("Helvetica", "", 10)
|
|
if section.Content != "" {
|
|
pdf.MultiCell(0, 5, section.Content, "", "L", false)
|
|
} else {
|
|
pdf.SetFont("Helvetica", "I", 10)
|
|
pdf.SetTextColor(150, 150, 150)
|
|
pdf.CellFormat(0, 7, "(Kein Inhalt vorhanden)", "", 1, "L", false, 0, "")
|
|
pdf.SetTextColor(0, 0, 0)
|
|
}
|
|
}
|
|
|
|
// buildAssessmentMap creates a lookup from HazardID to its most recent RiskAssessment
|
|
func buildAssessmentMap(assessments []RiskAssessment) map[string]*RiskAssessment {
|
|
m := make(map[string]*RiskAssessment)
|
|
for i := range assessments {
|
|
a := &assessments[i]
|
|
key := a.HazardID.String()
|
|
if existing, ok := m[key]; !ok || a.Version > existing.Version {
|
|
m[key] = a
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (e *DocumentExporter) pdfHazardLog(pdf *gofpdf.Fpdf, hazards []Hazard, assessments []RiskAssessment) {
|
|
pdf.SetFont("Helvetica", "B", 14)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.CellFormat(0, 10, "Gefaehrdungsprotokoll", "", 1, "L", false, 0, "")
|
|
pdf.SetTextColor(0, 0, 0)
|
|
pdf.SetDrawColor(200, 200, 200)
|
|
pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
|
|
pdf.Ln(5)
|
|
|
|
if len(hazards) == 0 {
|
|
pdf.SetFont("Helvetica", "I", 10)
|
|
pdf.CellFormat(0, 7, "(Keine Gefaehrdungen erfasst)", "", 1, "L", false, 0, "")
|
|
return
|
|
}
|
|
|
|
assessMap := buildAssessmentMap(assessments)
|
|
|
|
// Table header
|
|
colWidths := []float64{10, 40, 30, 12, 12, 12, 30, 20}
|
|
headers := []string{"Nr", "Name", "Kategorie", "S", "E", "P", "Risiko", "OK"}
|
|
|
|
pdf.SetFont("Helvetica", "B", 9)
|
|
pdf.SetFillColor(240, 240, 240)
|
|
for i, h := range headers {
|
|
pdf.CellFormat(colWidths[i], 7, h, "1", 0, "C", true, 0, "")
|
|
}
|
|
pdf.Ln(-1)
|
|
|
|
pdf.SetFont("Helvetica", "", 8)
|
|
for i, hazard := range hazards {
|
|
if pdf.GetY() > 265 {
|
|
pdf.AddPage()
|
|
// Reprint header
|
|
pdf.SetFont("Helvetica", "B", 9)
|
|
pdf.SetFillColor(240, 240, 240)
|
|
for j, h := range headers {
|
|
pdf.CellFormat(colWidths[j], 7, h, "1", 0, "C", true, 0, "")
|
|
}
|
|
pdf.Ln(-1)
|
|
pdf.SetFont("Helvetica", "", 8)
|
|
}
|
|
|
|
a := assessMap[hazard.ID.String()]
|
|
|
|
sev, exp, prob := "", "", ""
|
|
riskLabel := "-"
|
|
acceptable := "-"
|
|
var rl RiskLevel
|
|
|
|
if a != nil {
|
|
sev = fmt.Sprintf("%d", a.Severity)
|
|
exp = fmt.Sprintf("%d", a.Exposure)
|
|
prob = fmt.Sprintf("%d", a.Probability)
|
|
rl = a.RiskLevel
|
|
riskLabel = riskLevelLabel(rl)
|
|
if a.IsAcceptable {
|
|
acceptable = "Ja"
|
|
} else {
|
|
acceptable = "Nein"
|
|
}
|
|
}
|
|
|
|
// Color-code the row based on risk level
|
|
r, g, b := riskLevelColor(rl)
|
|
pdf.SetFillColor(r, g, b)
|
|
fill := rl != ""
|
|
|
|
pdf.CellFormat(colWidths[0], 6, fmt.Sprintf("%d", i+1), "1", 0, "C", fill, 0, "")
|
|
pdf.CellFormat(colWidths[1], 6, pdfTruncate(hazard.Name, 22), "1", 0, "L", fill, 0, "")
|
|
pdf.CellFormat(colWidths[2], 6, pdfTruncate(hazard.Category, 16), "1", 0, "L", fill, 0, "")
|
|
pdf.CellFormat(colWidths[3], 6, sev, "1", 0, "C", fill, 0, "")
|
|
pdf.CellFormat(colWidths[4], 6, exp, "1", 0, "C", fill, 0, "")
|
|
pdf.CellFormat(colWidths[5], 6, prob, "1", 0, "C", fill, 0, "")
|
|
pdf.CellFormat(colWidths[6], 6, riskLabel, "1", 0, "C", fill, 0, "")
|
|
pdf.CellFormat(colWidths[7], 6, acceptable, "1", 0, "C", fill, 0, "")
|
|
pdf.Ln(-1)
|
|
}
|
|
}
|
|
|
|
func (e *DocumentExporter) pdfRiskMatrixSummary(pdf *gofpdf.Fpdf, assessments []RiskAssessment) {
|
|
pdf.Ln(10)
|
|
pdf.SetFont("Helvetica", "B", 14)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.CellFormat(0, 10, "Risikomatrix-Zusammenfassung", "", 1, "L", false, 0, "")
|
|
pdf.SetTextColor(0, 0, 0)
|
|
pdf.SetDrawColor(200, 200, 200)
|
|
pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
|
|
pdf.Ln(5)
|
|
|
|
counts := countByRiskLevel(assessments)
|
|
|
|
levels := []RiskLevel{
|
|
RiskLevelNotAcceptable,
|
|
RiskLevelVeryHigh,
|
|
RiskLevelCritical,
|
|
RiskLevelHigh,
|
|
RiskLevelMedium,
|
|
RiskLevelLow,
|
|
RiskLevelNegligible,
|
|
}
|
|
|
|
pdf.SetFont("Helvetica", "B", 9)
|
|
pdf.SetFillColor(240, 240, 240)
|
|
pdf.CellFormat(60, 7, "Risikostufe", "1", 0, "L", true, 0, "")
|
|
pdf.CellFormat(30, 7, "Anzahl", "1", 0, "C", true, 0, "")
|
|
pdf.Ln(-1)
|
|
|
|
pdf.SetFont("Helvetica", "", 9)
|
|
for _, level := range levels {
|
|
count := counts[level]
|
|
if count == 0 {
|
|
continue
|
|
}
|
|
r, g, b := riskLevelColor(level)
|
|
pdf.SetFillColor(r, g, b)
|
|
pdf.CellFormat(60, 6, riskLevelLabel(level), "1", 0, "L", true, 0, "")
|
|
pdf.CellFormat(30, 6, fmt.Sprintf("%d", count), "1", 0, "C", true, 0, "")
|
|
pdf.Ln(-1)
|
|
}
|
|
|
|
pdf.SetFont("Helvetica", "B", 9)
|
|
pdf.SetFillColor(240, 240, 240)
|
|
pdf.CellFormat(60, 7, "Gesamt", "1", 0, "L", true, 0, "")
|
|
pdf.CellFormat(30, 7, fmt.Sprintf("%d", len(assessments)), "1", 0, "C", true, 0, "")
|
|
pdf.Ln(-1)
|
|
}
|
|
|
|
func (e *DocumentExporter) pdfMitigationsTable(pdf *gofpdf.Fpdf, mitigations []Mitigation) {
|
|
pdf.SetFont("Helvetica", "B", 14)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.CellFormat(0, 10, "Massnahmen-Uebersicht", "", 1, "L", false, 0, "")
|
|
pdf.SetTextColor(0, 0, 0)
|
|
pdf.SetDrawColor(200, 200, 200)
|
|
pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
|
|
pdf.Ln(5)
|
|
|
|
if len(mitigations) == 0 {
|
|
pdf.SetFont("Helvetica", "I", 10)
|
|
pdf.CellFormat(0, 7, "(Keine Massnahmen erfasst)", "", 1, "L", false, 0, "")
|
|
return
|
|
}
|
|
|
|
colWidths := []float64{10, 45, 30, 30, 40}
|
|
headers := []string{"Nr", "Name", "Typ", "Status", "Verifikation"}
|
|
|
|
pdf.SetFont("Helvetica", "B", 9)
|
|
pdf.SetFillColor(240, 240, 240)
|
|
for i, h := range headers {
|
|
pdf.CellFormat(colWidths[i], 7, h, "1", 0, "C", true, 0, "")
|
|
}
|
|
pdf.Ln(-1)
|
|
|
|
pdf.SetFont("Helvetica", "", 8)
|
|
for i, m := range mitigations {
|
|
if pdf.GetY() > 265 {
|
|
pdf.AddPage()
|
|
pdf.SetFont("Helvetica", "B", 9)
|
|
pdf.SetFillColor(240, 240, 240)
|
|
for j, h := range headers {
|
|
pdf.CellFormat(colWidths[j], 7, h, "1", 0, "C", true, 0, "")
|
|
}
|
|
pdf.Ln(-1)
|
|
pdf.SetFont("Helvetica", "", 8)
|
|
}
|
|
|
|
pdf.CellFormat(colWidths[0], 6, fmt.Sprintf("%d", i+1), "1", 0, "C", false, 0, "")
|
|
pdf.CellFormat(colWidths[1], 6, pdfTruncate(m.Name, 25), "1", 0, "L", false, 0, "")
|
|
pdf.CellFormat(colWidths[2], 6, reductionTypeLabel(m.ReductionType), "1", 0, "C", false, 0, "")
|
|
pdf.CellFormat(colWidths[3], 6, mitigationStatusLabel(m.Status), "1", 0, "C", false, 0, "")
|
|
pdf.CellFormat(colWidths[4], 6, pdfTruncate(string(m.VerificationMethod), 22), "1", 0, "L", false, 0, "")
|
|
pdf.Ln(-1)
|
|
}
|
|
}
|
|
|
|
func (e *DocumentExporter) pdfClassifications(pdf *gofpdf.Fpdf, classifications []RegulatoryClassification) {
|
|
pdf.SetFont("Helvetica", "B", 14)
|
|
pdf.SetTextColor(50, 50, 50)
|
|
pdf.CellFormat(0, 10, "Regulatorische Klassifizierungen", "", 1, "L", false, 0, "")
|
|
pdf.SetTextColor(0, 0, 0)
|
|
pdf.SetDrawColor(200, 200, 200)
|
|
pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
|
|
pdf.Ln(5)
|
|
|
|
for _, c := range classifications {
|
|
pdf.SetFont("Helvetica", "B", 11)
|
|
pdf.CellFormat(0, 7, regulationLabel(c.Regulation), "", 1, "L", false, 0, "")
|
|
|
|
pdf.SetFont("Helvetica", "", 10)
|
|
pdf.CellFormat(50, 6, "Klassifizierung:", "", 0, "L", false, 0, "")
|
|
pdf.CellFormat(0, 6, c.ClassificationResult, "", 1, "L", false, 0, "")
|
|
|
|
pdf.CellFormat(50, 6, "Risikostufe:", "", 0, "L", false, 0, "")
|
|
pdf.CellFormat(0, 6, riskLevelLabel(c.RiskLevel), "", 1, "L", false, 0, "")
|
|
|
|
if c.Reasoning != "" {
|
|
pdf.CellFormat(50, 6, "Begruendung:", "", 0, "L", false, 0, "")
|
|
pdf.MultiCell(0, 5, c.Reasoning, "", "L", false)
|
|
}
|
|
pdf.Ln(5)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Excel Export
|
|
// ============================================================================
|
|
|
|
// ExportExcel generates an XLSX workbook with project data across multiple sheets
|
|
func (e *DocumentExporter) ExportExcel(
|
|
project *Project,
|
|
sections []TechFileSection,
|
|
hazards []Hazard,
|
|
assessments []RiskAssessment,
|
|
mitigations []Mitigation,
|
|
) ([]byte, error) {
|
|
if project == nil {
|
|
return nil, fmt.Errorf("project must not be nil")
|
|
}
|
|
|
|
f := excelize.NewFile()
|
|
defer f.Close()
|
|
|
|
// --- Sheet 1: Uebersicht ---
|
|
overviewSheet := "Uebersicht"
|
|
f.SetSheetName("Sheet1", overviewSheet)
|
|
e.xlsxOverview(f, overviewSheet, project)
|
|
|
|
// --- Sheet 2: Gefaehrdungsprotokoll ---
|
|
hazardSheet := "Gefaehrdungsprotokoll"
|
|
f.NewSheet(hazardSheet)
|
|
e.xlsxHazardLog(f, hazardSheet, hazards, assessments)
|
|
|
|
// --- Sheet 3: Massnahmen ---
|
|
mitigationSheet := "Massnahmen"
|
|
f.NewSheet(mitigationSheet)
|
|
e.xlsxMitigations(f, mitigationSheet, mitigations)
|
|
|
|
// --- Sheet 4: Risikomatrix ---
|
|
matrixSheet := "Risikomatrix"
|
|
f.NewSheet(matrixSheet)
|
|
e.xlsxRiskMatrix(f, matrixSheet, assessments)
|
|
|
|
// --- Sheet 5: Sektionen ---
|
|
sectionSheet := "Sektionen"
|
|
f.NewSheet(sectionSheet)
|
|
e.xlsxSections(f, sectionSheet, sections)
|
|
|
|
buf, err := f.WriteToBuffer()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to write Excel: %w", err)
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func (e *DocumentExporter) xlsxOverview(f *excelize.File, sheet string, project *Project) {
|
|
headerStyle, _ := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Size: 11},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"D9E1F2"}},
|
|
})
|
|
|
|
f.SetColWidth(sheet, "A", "A", 30)
|
|
f.SetColWidth(sheet, "B", "B", 50)
|
|
|
|
rows := [][]string{
|
|
{"Eigenschaft", "Wert"},
|
|
{"Maschinenname", project.MachineName},
|
|
{"Maschinentyp", project.MachineType},
|
|
{"Hersteller", project.Manufacturer},
|
|
{"Beschreibung", project.Description},
|
|
{"CE-Kennzeichnungsziel", project.CEMarkingTarget},
|
|
{"Projektstatus", string(project.Status)},
|
|
{"Vollstaendigkeits-Score", fmt.Sprintf("%.1f%%", project.CompletenessScore*100)},
|
|
{"Erstellt am", project.CreatedAt.Format("02.01.2006 15:04")},
|
|
{"Aktualisiert am", project.UpdatedAt.Format("02.01.2006 15:04")},
|
|
}
|
|
|
|
for i, row := range rows {
|
|
rowNum := i + 1
|
|
f.SetCellValue(sheet, cellRef("A", rowNum), row[0])
|
|
f.SetCellValue(sheet, cellRef("B", rowNum), row[1])
|
|
if i == 0 {
|
|
f.SetCellStyle(sheet, cellRef("A", rowNum), cellRef("B", rowNum), headerStyle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *DocumentExporter) xlsxHazardLog(f *excelize.File, sheet string, hazards []Hazard, assessments []RiskAssessment) {
|
|
headerStyle, _ := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
|
|
Alignment: &excelize.Alignment{Horizontal: "center"},
|
|
})
|
|
|
|
headers := []string{"Nr", "Name", "Kategorie", "Beschreibung", "S", "E", "P", "A",
|
|
"Inherent Risk", "C_eff", "Residual Risk", "Risk Level", "Akzeptabel"}
|
|
|
|
colWidths := map[string]float64{
|
|
"A": 6, "B": 25, "C": 20, "D": 35, "E": 8, "F": 8, "G": 8, "H": 8,
|
|
"I": 14, "J": 10, "K": 14, "L": 18, "M": 12,
|
|
}
|
|
for col, w := range colWidths {
|
|
f.SetColWidth(sheet, col, col, w)
|
|
}
|
|
|
|
cols := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"}
|
|
for i, h := range headers {
|
|
f.SetCellValue(sheet, cellRef(cols[i], 1), h)
|
|
}
|
|
f.SetCellStyle(sheet, "A1", cellRef(cols[len(cols)-1], 1), headerStyle)
|
|
|
|
assessMap := buildAssessmentMap(assessments)
|
|
|
|
for i, hazard := range hazards {
|
|
row := i + 2
|
|
a := assessMap[hazard.ID.String()]
|
|
|
|
f.SetCellValue(sheet, cellRef("A", row), i+1)
|
|
f.SetCellValue(sheet, cellRef("B", row), hazard.Name)
|
|
f.SetCellValue(sheet, cellRef("C", row), hazard.Category)
|
|
f.SetCellValue(sheet, cellRef("D", row), hazard.Description)
|
|
|
|
if a != nil {
|
|
f.SetCellValue(sheet, cellRef("E", row), a.Severity)
|
|
f.SetCellValue(sheet, cellRef("F", row), a.Exposure)
|
|
f.SetCellValue(sheet, cellRef("G", row), a.Probability)
|
|
f.SetCellValue(sheet, cellRef("H", row), a.Avoidance)
|
|
f.SetCellValue(sheet, cellRef("I", row), fmt.Sprintf("%.1f", a.InherentRisk))
|
|
f.SetCellValue(sheet, cellRef("J", row), fmt.Sprintf("%.2f", a.CEff))
|
|
f.SetCellValue(sheet, cellRef("K", row), fmt.Sprintf("%.1f", a.ResidualRisk))
|
|
f.SetCellValue(sheet, cellRef("L", row), riskLevelLabel(a.RiskLevel))
|
|
|
|
acceptStr := "Nein"
|
|
if a.IsAcceptable {
|
|
acceptStr = "Ja"
|
|
}
|
|
f.SetCellValue(sheet, cellRef("M", row), acceptStr)
|
|
|
|
// Color-code the risk level cell
|
|
r, g, b := riskLevelColor(a.RiskLevel)
|
|
style, _ := f.NewStyle(&excelize.Style{
|
|
Fill: excelize.Fill{
|
|
Type: "pattern",
|
|
Pattern: 1,
|
|
Color: []string{rgbHex(r, g, b)},
|
|
},
|
|
Alignment: &excelize.Alignment{Horizontal: "center"},
|
|
})
|
|
f.SetCellStyle(sheet, cellRef("L", row), cellRef("L", row), style)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *DocumentExporter) xlsxMitigations(f *excelize.File, sheet string, mitigations []Mitigation) {
|
|
headerStyle, _ := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
|
|
Alignment: &excelize.Alignment{Horizontal: "center"},
|
|
})
|
|
|
|
headers := []string{"Nr", "Name", "Typ", "Beschreibung", "Status", "Verifikationsmethode", "Ergebnis"}
|
|
cols := []string{"A", "B", "C", "D", "E", "F", "G"}
|
|
|
|
f.SetColWidth(sheet, "A", "A", 6)
|
|
f.SetColWidth(sheet, "B", "B", 25)
|
|
f.SetColWidth(sheet, "C", "C", 15)
|
|
f.SetColWidth(sheet, "D", "D", 35)
|
|
f.SetColWidth(sheet, "E", "E", 15)
|
|
f.SetColWidth(sheet, "F", "F", 22)
|
|
f.SetColWidth(sheet, "G", "G", 25)
|
|
|
|
for i, h := range headers {
|
|
f.SetCellValue(sheet, cellRef(cols[i], 1), h)
|
|
}
|
|
f.SetCellStyle(sheet, "A1", cellRef(cols[len(cols)-1], 1), headerStyle)
|
|
|
|
for i, m := range mitigations {
|
|
row := i + 2
|
|
f.SetCellValue(sheet, cellRef("A", row), i+1)
|
|
f.SetCellValue(sheet, cellRef("B", row), m.Name)
|
|
f.SetCellValue(sheet, cellRef("C", row), reductionTypeLabel(m.ReductionType))
|
|
f.SetCellValue(sheet, cellRef("D", row), m.Description)
|
|
f.SetCellValue(sheet, cellRef("E", row), mitigationStatusLabel(m.Status))
|
|
f.SetCellValue(sheet, cellRef("F", row), string(m.VerificationMethod))
|
|
f.SetCellValue(sheet, cellRef("G", row), m.VerificationResult)
|
|
}
|
|
}
|
|
|
|
func (e *DocumentExporter) xlsxRiskMatrix(f *excelize.File, sheet string, assessments []RiskAssessment) {
|
|
headerStyle, _ := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
|
|
Alignment: &excelize.Alignment{Horizontal: "center"},
|
|
})
|
|
|
|
f.SetColWidth(sheet, "A", "A", 25)
|
|
f.SetColWidth(sheet, "B", "B", 12)
|
|
|
|
f.SetCellValue(sheet, "A1", "Risikostufe")
|
|
f.SetCellValue(sheet, "B1", "Anzahl")
|
|
f.SetCellStyle(sheet, "A1", "B1", headerStyle)
|
|
|
|
counts := countByRiskLevel(assessments)
|
|
|
|
levels := []RiskLevel{
|
|
RiskLevelNotAcceptable,
|
|
RiskLevelVeryHigh,
|
|
RiskLevelCritical,
|
|
RiskLevelHigh,
|
|
RiskLevelMedium,
|
|
RiskLevelLow,
|
|
RiskLevelNegligible,
|
|
}
|
|
|
|
row := 2
|
|
for _, level := range levels {
|
|
count := counts[level]
|
|
f.SetCellValue(sheet, cellRef("A", row), riskLevelLabel(level))
|
|
f.SetCellValue(sheet, cellRef("B", row), count)
|
|
|
|
r, g, b := riskLevelColor(level)
|
|
style, _ := f.NewStyle(&excelize.Style{
|
|
Fill: excelize.Fill{
|
|
Type: "pattern",
|
|
Pattern: 1,
|
|
Color: []string{rgbHex(r, g, b)},
|
|
},
|
|
})
|
|
f.SetCellStyle(sheet, cellRef("A", row), cellRef("B", row), style)
|
|
row++
|
|
}
|
|
|
|
// Total row
|
|
totalStyle, _ := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"D9E1F2"}},
|
|
})
|
|
f.SetCellValue(sheet, cellRef("A", row), "Gesamt")
|
|
f.SetCellValue(sheet, cellRef("B", row), len(assessments))
|
|
f.SetCellStyle(sheet, cellRef("A", row), cellRef("B", row), totalStyle)
|
|
}
|
|
|
|
func (e *DocumentExporter) xlsxSections(f *excelize.File, sheet string, sections []TechFileSection) {
|
|
headerStyle, _ := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
|
|
Alignment: &excelize.Alignment{Horizontal: "center"},
|
|
})
|
|
|
|
f.SetColWidth(sheet, "A", "A", 25)
|
|
f.SetColWidth(sheet, "B", "B", 40)
|
|
f.SetColWidth(sheet, "C", "C", 15)
|
|
|
|
headers := []string{"Sektion", "Titel", "Status"}
|
|
cols := []string{"A", "B", "C"}
|
|
|
|
for i, h := range headers {
|
|
f.SetCellValue(sheet, cellRef(cols[i], 1), h)
|
|
}
|
|
f.SetCellStyle(sheet, "A1", cellRef(cols[len(cols)-1], 1), headerStyle)
|
|
|
|
for i, s := range sections {
|
|
row := i + 2
|
|
f.SetCellValue(sheet, cellRef("A", row), s.SectionType)
|
|
f.SetCellValue(sheet, cellRef("B", row), s.Title)
|
|
f.SetCellValue(sheet, cellRef("C", row), string(s.Status))
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Markdown Export
|
|
// ============================================================================
|
|
|
|
// ExportMarkdown generates a Markdown document of the CE technical file sections
|
|
func (e *DocumentExporter) ExportMarkdown(
|
|
project *Project,
|
|
sections []TechFileSection,
|
|
) ([]byte, error) {
|
|
if project == nil {
|
|
return nil, fmt.Errorf("project must not be nil")
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
// Title
|
|
buf.WriteString(fmt.Sprintf("# CE-Akte: %s\n\n", project.MachineName))
|
|
|
|
// Metadata block
|
|
buf.WriteString("| Eigenschaft | Wert |\n")
|
|
buf.WriteString("|-------------|------|\n")
|
|
buf.WriteString(fmt.Sprintf("| Hersteller | %s |\n", project.Manufacturer))
|
|
buf.WriteString(fmt.Sprintf("| Maschinentyp | %s |\n", project.MachineType))
|
|
if project.CEMarkingTarget != "" {
|
|
buf.WriteString(fmt.Sprintf("| CE-Kennzeichnungsziel | %s |\n", project.CEMarkingTarget))
|
|
}
|
|
buf.WriteString(fmt.Sprintf("| Status | %s |\n", project.Status))
|
|
buf.WriteString(fmt.Sprintf("| Datum | %s |\n", time.Now().Format("02.01.2006")))
|
|
buf.WriteString("\n")
|
|
|
|
if project.Description != "" {
|
|
buf.WriteString(fmt.Sprintf("> %s\n\n", project.Description))
|
|
}
|
|
|
|
// Sections
|
|
for _, section := range sections {
|
|
buf.WriteString(fmt.Sprintf("## %s\n\n", section.Title))
|
|
buf.WriteString(fmt.Sprintf("*Typ: %s | Status: %s | Version: %d*\n\n",
|
|
section.SectionType, string(section.Status), section.Version))
|
|
if section.Content != "" {
|
|
buf.WriteString(section.Content)
|
|
buf.WriteString("\n\n")
|
|
} else {
|
|
buf.WriteString("*(Kein Inhalt vorhanden)*\n\n")
|
|
}
|
|
}
|
|
|
|
// Footer
|
|
buf.WriteString("---\n\n")
|
|
buf.WriteString(fmt.Sprintf("*Generiert am %s mit BreakPilot AI Compliance SDK*\n",
|
|
time.Now().Format("02.01.2006 15:04")))
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// ============================================================================
|
|
// DOCX Export (minimal OOXML via archive/zip)
|
|
// ============================================================================
|
|
|
|
// ExportDOCX generates a minimal DOCX file containing the CE technical file sections
|
|
func (e *DocumentExporter) ExportDOCX(
|
|
project *Project,
|
|
sections []TechFileSection,
|
|
) ([]byte, error) {
|
|
if project == nil {
|
|
return nil, fmt.Errorf("project must not be nil")
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
|
|
// [Content_Types].xml
|
|
contentTypes := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
<Default Extension="xml" ContentType="application/xml"/>
|
|
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
|
</Types>`
|
|
if err := addZipEntry(zw, "[Content_Types].xml", contentTypes); err != nil {
|
|
return nil, fmt.Errorf("failed to write [Content_Types].xml: %w", err)
|
|
}
|
|
|
|
// _rels/.rels
|
|
rels := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
|
</Relationships>`
|
|
if err := addZipEntry(zw, "_rels/.rels", rels); err != nil {
|
|
return nil, fmt.Errorf("failed to write _rels/.rels: %w", err)
|
|
}
|
|
|
|
// word/_rels/document.xml.rels
|
|
docRels := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
</Relationships>`
|
|
if err := addZipEntry(zw, "word/_rels/document.xml.rels", docRels); err != nil {
|
|
return nil, fmt.Errorf("failed to write word/_rels/document.xml.rels: %w", err)
|
|
}
|
|
|
|
// word/document.xml — build the body
|
|
docXML := e.buildDocumentXML(project, sections)
|
|
if err := addZipEntry(zw, "word/document.xml", docXML); err != nil {
|
|
return nil, fmt.Errorf("failed to write word/document.xml: %w", err)
|
|
}
|
|
|
|
if err := zw.Close(); err != nil {
|
|
return nil, fmt.Errorf("failed to close ZIP: %w", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func (e *DocumentExporter) buildDocumentXML(project *Project, sections []TechFileSection) string {
|
|
var body strings.Builder
|
|
|
|
// Title paragraph (Heading 1 style)
|
|
body.WriteString(docxHeading(fmt.Sprintf("CE-Akte: %s", project.MachineName), 1))
|
|
|
|
// Metadata paragraphs
|
|
metaLines := []string{
|
|
fmt.Sprintf("Hersteller: %s", project.Manufacturer),
|
|
fmt.Sprintf("Maschinentyp: %s", project.MachineType),
|
|
}
|
|
if project.CEMarkingTarget != "" {
|
|
metaLines = append(metaLines, fmt.Sprintf("CE-Kennzeichnungsziel: %s", project.CEMarkingTarget))
|
|
}
|
|
metaLines = append(metaLines,
|
|
fmt.Sprintf("Status: %s", project.Status),
|
|
fmt.Sprintf("Datum: %s", time.Now().Format("02.01.2006")),
|
|
)
|
|
for _, line := range metaLines {
|
|
body.WriteString(docxParagraph(line, false))
|
|
}
|
|
|
|
if project.Description != "" {
|
|
body.WriteString(docxParagraph("", false)) // blank line
|
|
body.WriteString(docxParagraph(project.Description, true))
|
|
}
|
|
|
|
// Sections
|
|
for _, section := range sections {
|
|
body.WriteString(docxHeading(section.Title, 2))
|
|
body.WriteString(docxParagraph(
|
|
fmt.Sprintf("Typ: %s | Status: %s | Version: %d",
|
|
section.SectionType, string(section.Status), section.Version),
|
|
true,
|
|
))
|
|
|
|
if section.Content != "" {
|
|
// Split content by newlines into separate paragraphs
|
|
for _, line := range strings.Split(section.Content, "\n") {
|
|
body.WriteString(docxParagraph(line, false))
|
|
}
|
|
} else {
|
|
body.WriteString(docxParagraph("(Kein Inhalt vorhanden)", true))
|
|
}
|
|
}
|
|
|
|
// Footer
|
|
body.WriteString(docxParagraph("", false))
|
|
body.WriteString(docxParagraph(
|
|
fmt.Sprintf("Generiert am %s mit BreakPilot AI Compliance SDK",
|
|
time.Now().Format("02.01.2006 15:04")),
|
|
true,
|
|
))
|
|
|
|
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
|
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
|
|
xmlns:v="urn:schemas-microsoft-com:vml"
|
|
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
|
xmlns:w10="urn:schemas-microsoft-com:office:word"
|
|
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
|
|
xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
|
|
xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
|
|
xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
|
|
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
|
|
mc:Ignorable="w14 wp14">
|
|
<w:body>
|
|
%s
|
|
</w:body>
|
|
</w:document>`, body.String())
|
|
}
|
|
|
|
// ============================================================================
|
|
// JSON Export (convenience — returns marshalled project data)
|
|
// ============================================================================
|
|
|
|
// ExportJSON returns a JSON representation of the project export data
|
|
func (e *DocumentExporter) ExportJSON(
|
|
project *Project,
|
|
sections []TechFileSection,
|
|
hazards []Hazard,
|
|
assessments []RiskAssessment,
|
|
mitigations []Mitigation,
|
|
classifications []RegulatoryClassification,
|
|
) ([]byte, error) {
|
|
if project == nil {
|
|
return nil, fmt.Errorf("project must not be nil")
|
|
}
|
|
|
|
payload := map[string]interface{}{
|
|
"project": project,
|
|
"sections": sections,
|
|
"hazards": hazards,
|
|
"assessments": assessments,
|
|
"mitigations": mitigations,
|
|
"classifications": classifications,
|
|
"exported_at": time.Now().UTC().Format(time.RFC3339),
|
|
"format_version": "1.0",
|
|
}
|
|
|
|
data, err := json.MarshalIndent(payload, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal JSON: %w", err)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
// riskLevelColor returns RGB values for color-coding a given risk level
|
|
func riskLevelColor(level RiskLevel) (r, g, b int) {
|
|
switch level {
|
|
case RiskLevelNotAcceptable:
|
|
return 180, 0, 0 // dark red
|
|
case RiskLevelVeryHigh:
|
|
return 220, 40, 40 // red
|
|
case RiskLevelCritical:
|
|
return 255, 80, 80 // bright red
|
|
case RiskLevelHigh:
|
|
return 255, 165, 80 // orange
|
|
case RiskLevelMedium:
|
|
return 255, 230, 100 // yellow
|
|
case RiskLevelLow:
|
|
return 180, 230, 140 // light green
|
|
case RiskLevelNegligible:
|
|
return 140, 210, 140 // green
|
|
default:
|
|
return 240, 240, 240 // light gray (unassessed)
|
|
}
|
|
}
|
|
|
|
// riskLevelLabel returns a German display label for a risk level
|
|
func riskLevelLabel(level RiskLevel) string {
|
|
switch level {
|
|
case RiskLevelNotAcceptable:
|
|
return "Nicht akzeptabel"
|
|
case RiskLevelVeryHigh:
|
|
return "Sehr hoch"
|
|
case RiskLevelCritical:
|
|
return "Kritisch"
|
|
case RiskLevelHigh:
|
|
return "Hoch"
|
|
case RiskLevelMedium:
|
|
return "Mittel"
|
|
case RiskLevelLow:
|
|
return "Niedrig"
|
|
case RiskLevelNegligible:
|
|
return "Vernachlaessigbar"
|
|
default:
|
|
return string(level)
|
|
}
|
|
}
|
|
|
|
// reductionTypeLabel returns a German label for a reduction type
|
|
func reductionTypeLabel(rt ReductionType) string {
|
|
switch rt {
|
|
case ReductionTypeDesign:
|
|
return "Konstruktiv"
|
|
case ReductionTypeProtective:
|
|
return "Schutzmassnahme"
|
|
case ReductionTypeInformation:
|
|
return "Information"
|
|
default:
|
|
return string(rt)
|
|
}
|
|
}
|
|
|
|
// mitigationStatusLabel returns a German label for a mitigation status
|
|
func mitigationStatusLabel(status MitigationStatus) string {
|
|
switch status {
|
|
case MitigationStatusPlanned:
|
|
return "Geplant"
|
|
case MitigationStatusImplemented:
|
|
return "Umgesetzt"
|
|
case MitigationStatusVerified:
|
|
return "Verifiziert"
|
|
case MitigationStatusRejected:
|
|
return "Abgelehnt"
|
|
default:
|
|
return string(status)
|
|
}
|
|
}
|
|
|
|
// regulationLabel returns a German label for a regulation type
|
|
func regulationLabel(reg RegulationType) string {
|
|
switch reg {
|
|
case RegulationNIS2:
|
|
return "NIS-2 Richtlinie"
|
|
case RegulationAIAct:
|
|
return "EU AI Act"
|
|
case RegulationCRA:
|
|
return "Cyber Resilience Act"
|
|
case RegulationMachineryRegulation:
|
|
return "EU Maschinenverordnung 2023/1230"
|
|
default:
|
|
return string(reg)
|
|
}
|
|
}
|
|
|
|
// escapeXML escapes special XML characters in text content
|
|
func escapeXML(s string) string {
|
|
var buf bytes.Buffer
|
|
if err := xml.EscapeText(&buf, []byte(s)); err != nil {
|
|
// Fallback: return input unchanged (xml.EscapeText should never error on valid UTF-8)
|
|
return s
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// countByRiskLevel counts assessments per risk level
|
|
func countByRiskLevel(assessments []RiskAssessment) map[RiskLevel]int {
|
|
counts := make(map[RiskLevel]int)
|
|
for _, a := range assessments {
|
|
counts[a.RiskLevel]++
|
|
}
|
|
return counts
|
|
}
|
|
|
|
// pdfTruncate truncates a string for PDF cell display
|
|
func pdfTruncate(s string, maxLen int) string {
|
|
runes := []rune(s)
|
|
if len(runes) <= maxLen {
|
|
return s
|
|
}
|
|
if maxLen <= 3 {
|
|
return string(runes[:maxLen])
|
|
}
|
|
return string(runes[:maxLen-3]) + "..."
|
|
}
|
|
|
|
// cellRef builds an Excel cell reference like "A1", "B12"
|
|
func cellRef(col string, row int) string {
|
|
return fmt.Sprintf("%s%d", col, row)
|
|
}
|
|
|
|
// rgbHex converts RGB values to a hex color string (without #)
|
|
func rgbHex(r, g, b int) string {
|
|
return fmt.Sprintf("%02X%02X%02X", r, g, b)
|
|
}
|
|
|
|
// addZipEntry writes a text file into a zip archive
|
|
func addZipEntry(zw *zip.Writer, name, content string) error {
|
|
w, err := zw.Create(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = w.Write([]byte(content))
|
|
return err
|
|
}
|
|
|
|
// docxHeading builds a DOCX paragraph with a heading style
|
|
func docxHeading(text string, level int) string {
|
|
// Map level to font size (in half-points): 1→32pt, 2→26pt, 3→22pt
|
|
sizes := map[int]int{1: 64, 2: 52, 3: 44}
|
|
sz, ok := sizes[level]
|
|
if !ok {
|
|
sz = 44
|
|
}
|
|
escaped := escapeXML(text)
|
|
return fmt.Sprintf(` <w:p>
|
|
<w:pPr>
|
|
<w:pStyle w:val="Heading%d"/>
|
|
<w:spacing w:after="200"/>
|
|
</w:pPr>
|
|
<w:r>
|
|
<w:rPr><w:b/><w:sz w:val="%d"/><w:szCs w:val="%d"/></w:rPr>
|
|
<w:t xml:space="preserve">%s</w:t>
|
|
</w:r>
|
|
</w:p>
|
|
`, level, sz, sz, escaped)
|
|
}
|
|
|
|
// docxParagraph builds a DOCX paragraph, optionally italic
|
|
func docxParagraph(text string, italic bool) string {
|
|
escaped := escapeXML(text)
|
|
rpr := ""
|
|
if italic {
|
|
rpr = "<w:rPr><w:i/></w:rPr>"
|
|
}
|
|
return fmt.Sprintf(` <w:p>
|
|
<w:r>
|
|
%s
|
|
<w:t xml:space="preserve">%s</w:t>
|
|
</w:r>
|
|
</w:p>
|
|
`, rpr, escaped)
|
|
}
|