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 := ` ` 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 := ` ` 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 := ` ` 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(` %s `, 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(` %s `, 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 = "" } return fmt.Sprintf(` %s %s `, rpr, escaped) }