Some checks failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Add complete Academy backend (Go) and frontend (Next.js) for DSGVO/IT-Security/AI-Literacy compliance training: - Go backend: Course CRUD, enrollments, quiz evaluation, PDF certificates (gofpdf), video generation pipeline (ElevenLabs + HeyGen) - In-memory data store with PostgreSQL migration for future DB support - Frontend: Course creation (AI + manual), lesson viewer, interactive quiz, certificate viewer with PDF download - Fix existing compile errors in generate.go (SearchResult type mismatch), llm/service.go (unused var), rag/service.go (Unicode chars) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
153 lines
4.4 KiB
Go
153 lines
4.4 KiB
Go
package academy
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jung-kurt/gofpdf"
|
|
)
|
|
|
|
// CertificateData holds all data needed to generate a certificate PDF
|
|
type CertificateData struct {
|
|
CertificateID string
|
|
UserName string
|
|
CourseName string
|
|
CompanyName string
|
|
Score int
|
|
IssuedAt time.Time
|
|
ValidUntil time.Time
|
|
}
|
|
|
|
// GenerateCertificatePDF generates a PDF certificate and returns the bytes
|
|
func GenerateCertificatePDF(data CertificateData) ([]byte, error) {
|
|
pdf := gofpdf.New("L", "mm", "A4", "") // Landscape A4
|
|
pdf.SetAutoPageBreak(false, 0)
|
|
pdf.AddPage()
|
|
|
|
pageWidth, pageHeight := pdf.GetPageSize()
|
|
|
|
// Background color - light gray
|
|
pdf.SetFillColor(250, 250, 252)
|
|
pdf.Rect(0, 0, pageWidth, pageHeight, "F")
|
|
|
|
// Border - decorative
|
|
pdf.SetDrawColor(79, 70, 229) // Purple/Indigo
|
|
pdf.SetLineWidth(3)
|
|
pdf.Rect(10, 10, pageWidth-20, pageHeight-20, "D")
|
|
pdf.SetLineWidth(1)
|
|
pdf.Rect(14, 14, pageWidth-28, pageHeight-28, "D")
|
|
|
|
// Header - Company/BreakPilot Logo area
|
|
companyName := data.CompanyName
|
|
if companyName == "" {
|
|
companyName = "BreakPilot Compliance"
|
|
}
|
|
|
|
pdf.SetFont("Helvetica", "", 12)
|
|
pdf.SetTextColor(120, 120, 120)
|
|
pdf.SetXY(0, 25)
|
|
pdf.CellFormat(pageWidth, 10, companyName, "", 0, "C", false, 0, "")
|
|
|
|
// Title
|
|
pdf.SetFont("Helvetica", "B", 32)
|
|
pdf.SetTextColor(30, 30, 30)
|
|
pdf.SetXY(0, 42)
|
|
pdf.CellFormat(pageWidth, 15, "SCHULUNGSZERTIFIKAT", "", 0, "C", false, 0, "")
|
|
|
|
// Decorative line
|
|
pdf.SetDrawColor(79, 70, 229)
|
|
pdf.SetLineWidth(1.5)
|
|
lineY := 62.0
|
|
pdf.Line(pageWidth/2-60, lineY, pageWidth/2+60, lineY)
|
|
|
|
// "Hiermit wird bescheinigt, dass"
|
|
pdf.SetFont("Helvetica", "", 13)
|
|
pdf.SetTextColor(80, 80, 80)
|
|
pdf.SetXY(0, 72)
|
|
pdf.CellFormat(pageWidth, 8, "Hiermit wird bescheinigt, dass", "", 0, "C", false, 0, "")
|
|
|
|
// Name
|
|
pdf.SetFont("Helvetica", "B", 26)
|
|
pdf.SetTextColor(30, 30, 30)
|
|
pdf.SetXY(0, 85)
|
|
pdf.CellFormat(pageWidth, 12, data.UserName, "", 0, "C", false, 0, "")
|
|
|
|
// "die folgende Compliance-Schulung erfolgreich abgeschlossen hat:"
|
|
pdf.SetFont("Helvetica", "", 13)
|
|
pdf.SetTextColor(80, 80, 80)
|
|
pdf.SetXY(0, 103)
|
|
pdf.CellFormat(pageWidth, 8, "die folgende Compliance-Schulung erfolgreich abgeschlossen hat:", "", 0, "C", false, 0, "")
|
|
|
|
// Course Name
|
|
pdf.SetFont("Helvetica", "B", 20)
|
|
pdf.SetTextColor(79, 70, 229)
|
|
pdf.SetXY(0, 116)
|
|
pdf.CellFormat(pageWidth, 10, data.CourseName, "", 0, "C", false, 0, "")
|
|
|
|
// Score
|
|
if data.Score > 0 {
|
|
pdf.SetFont("Helvetica", "", 12)
|
|
pdf.SetTextColor(80, 80, 80)
|
|
pdf.SetXY(0, 130)
|
|
pdf.CellFormat(pageWidth, 8, fmt.Sprintf("Testergebnis: %d%%", data.Score), "", 0, "C", false, 0, "")
|
|
}
|
|
|
|
// Bottom section - Dates and Signature
|
|
bottomY := 148.0
|
|
|
|
// Left: Issued Date
|
|
pdf.SetFont("Helvetica", "", 10)
|
|
pdf.SetTextColor(100, 100, 100)
|
|
pdf.SetXY(40, bottomY)
|
|
pdf.CellFormat(80, 6, fmt.Sprintf("Abschlussdatum: %s", data.IssuedAt.Format("02.01.2006")), "", 0, "L", false, 0, "")
|
|
|
|
// Center: Valid Until
|
|
pdf.SetXY(pageWidth/2-40, bottomY)
|
|
pdf.CellFormat(80, 6, fmt.Sprintf("Gueltig bis: %s", data.ValidUntil.Format("02.01.2006")), "", 0, "C", false, 0, "")
|
|
|
|
// Right: Certificate ID
|
|
pdf.SetXY(pageWidth-120, bottomY)
|
|
pdf.CellFormat(80, 6, fmt.Sprintf("Zertifikats-Nr.: %s", data.CertificateID[:min(12, len(data.CertificateID))]), "", 0, "R", false, 0, "")
|
|
|
|
// Signature line
|
|
sigY := 162.0
|
|
pdf.SetDrawColor(150, 150, 150)
|
|
pdf.SetLineWidth(0.5)
|
|
|
|
// Left signature
|
|
pdf.Line(50, sigY, 130, sigY)
|
|
pdf.SetFont("Helvetica", "", 9)
|
|
pdf.SetTextColor(120, 120, 120)
|
|
pdf.SetXY(50, sigY+2)
|
|
pdf.CellFormat(80, 5, "Datenschutzbeauftragter", "", 0, "C", false, 0, "")
|
|
|
|
// Right signature
|
|
pdf.Line(pageWidth-130, sigY, pageWidth-50, sigY)
|
|
pdf.SetXY(pageWidth-130, sigY+2)
|
|
pdf.CellFormat(80, 5, "Geschaeftsfuehrung", "", 0, "C", false, 0, "")
|
|
|
|
// Footer
|
|
pdf.SetFont("Helvetica", "", 8)
|
|
pdf.SetTextColor(160, 160, 160)
|
|
pdf.SetXY(0, pageHeight-22)
|
|
pdf.CellFormat(pageWidth, 5, "Dieses Zertifikat wurde elektronisch erstellt und ist ohne Unterschrift gueltig.", "", 0, "C", false, 0, "")
|
|
pdf.SetXY(0, pageHeight-17)
|
|
pdf.CellFormat(pageWidth, 5, fmt.Sprintf("Verifizierung unter: https://compliance.breakpilot.de/verify/%s", data.CertificateID), "", 0, "C", false, 0, "")
|
|
|
|
// Generate PDF bytes
|
|
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 min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|