This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/ai-compliance-sdk/internal/funding/export.go
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

396 lines
12 KiB
Go

package funding
import (
"archive/zip"
"bytes"
"fmt"
"io"
"time"
"github.com/jung-kurt/gofpdf"
"github.com/xuri/excelize/v2"
)
// ExportService handles document generation
type ExportService struct{}
// NewExportService creates a new export service
func NewExportService() *ExportService {
return &ExportService{}
}
// GenerateApplicationLetter generates the main application letter as PDF
func (s *ExportService) GenerateApplicationLetter(app *FundingApplication) ([]byte, error) {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.SetMargins(25, 25, 25)
pdf.AddPage()
// Header
pdf.SetFont("Helvetica", "B", 14)
pdf.Cell(0, 10, "Antrag auf Foerderung im Rahmen der digitalen Bildungsinfrastruktur")
pdf.Ln(15)
// Application number
pdf.SetFont("Helvetica", "", 10)
pdf.Cell(0, 6, fmt.Sprintf("Antragsnummer: %s", app.ApplicationNumber))
pdf.Ln(6)
pdf.Cell(0, 6, fmt.Sprintf("Datum: %s", time.Now().Format("02.01.2006")))
pdf.Ln(15)
// Section 1: Einleitung
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "1. Einleitung")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.SchoolProfile != nil {
pdf.MultiCell(0, 6, fmt.Sprintf(
"Die %s (Schulnummer: %s) beantragt hiermit Foerdermittel aus dem Programm %s.\n\n"+
"Schultraeger: %s\n"+
"Schulform: %s\n"+
"Schueleranzahl: %d\n"+
"Lehrkraefte: %d",
app.SchoolProfile.Name,
app.SchoolProfile.SchoolNumber,
app.FundingProgram,
app.SchoolProfile.CarrierName,
app.SchoolProfile.Type,
app.SchoolProfile.StudentCount,
app.SchoolProfile.TeacherCount,
), "", "", false)
}
pdf.Ln(10)
// Section 2: Projektziel
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "2. Projektziel")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.ProjectPlan != nil {
pdf.MultiCell(0, 6, app.ProjectPlan.Summary, "", "", false)
pdf.Ln(5)
pdf.MultiCell(0, 6, app.ProjectPlan.Goals, "", "", false)
}
pdf.Ln(10)
// Section 3: Beschreibung der Massnahme
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "3. Beschreibung der Massnahme")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.ProjectPlan != nil {
pdf.MultiCell(0, 6, app.ProjectPlan.DidacticConcept, "", "", false)
}
pdf.Ln(10)
// Section 4: Datenschutz & IT-Betrieb
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "4. Datenschutz & IT-Betrieb")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.ProjectPlan != nil && app.ProjectPlan.DataProtection != "" {
pdf.MultiCell(0, 6, app.ProjectPlan.DataProtection, "", "", false)
}
pdf.Ln(10)
// Section 5: Kosten & Finanzierung
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "5. Kosten & Finanzierung")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.Budget != nil {
pdf.Cell(0, 6, fmt.Sprintf("Gesamtkosten: %.2f EUR", app.Budget.TotalCost))
pdf.Ln(6)
pdf.Cell(0, 6, fmt.Sprintf("Beantragter Foerderbetrag: %.2f EUR (%.0f%%)", app.Budget.RequestedFunding, app.Budget.FundingRate*100))
pdf.Ln(6)
pdf.Cell(0, 6, fmt.Sprintf("Eigenanteil: %.2f EUR", app.Budget.OwnContribution))
}
pdf.Ln(10)
// Section 6: Laufzeit
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "6. Laufzeit")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.Timeline != nil {
pdf.Cell(0, 6, fmt.Sprintf("Projektbeginn: %s", app.Timeline.PlannedStart.Format("02.01.2006")))
pdf.Ln(6)
pdf.Cell(0, 6, fmt.Sprintf("Projektende: %s", app.Timeline.PlannedEnd.Format("02.01.2006")))
}
pdf.Ln(15)
// Footer note
pdf.SetFont("Helvetica", "I", 9)
pdf.MultiCell(0, 5, "Hinweis: Dieser Antrag wurde mit dem Foerderantrag-Wizard von BreakPilot erstellt. "+
"Die finale Pruefung und Einreichung erfolgt durch den Schultraeger.", "", "", false)
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// GenerateBudgetPlan generates the budget plan as XLSX
func (s *ExportService) GenerateBudgetPlan(app *FundingApplication) ([]byte, error) {
f := excelize.NewFile()
sheetName := "Kostenplan"
f.SetSheetName("Sheet1", sheetName)
// Header row
headers := []string{
"Pos.", "Kategorie", "Beschreibung", "Hersteller",
"Anzahl", "Einzelpreis", "Gesamt", "Foerderfahig", "Finanzierung",
}
for i, h := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
f.SetCellValue(sheetName, cell, h)
}
// Style header
headerStyle, _ := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true},
Fill: excelize.Fill{Type: "pattern", Color: []string{"#E0E0E0"}, Pattern: 1},
})
f.SetRowStyle(sheetName, 1, 1, headerStyle)
// Data rows
row := 2
if app.Budget != nil {
for i, item := range app.Budget.BudgetItems {
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), i+1)
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), string(item.Category))
f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), item.Description)
f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), item.Manufacturer)
f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), item.Quantity)
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), item.UnitPrice)
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), item.TotalPrice)
fundable := "Nein"
if item.IsFundable {
fundable = "Ja"
}
f.SetCellValue(sheetName, fmt.Sprintf("H%d", row), fundable)
f.SetCellValue(sheetName, fmt.Sprintf("I%d", row), item.FundingSource)
row++
}
// Summary rows
row += 2
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), "Gesamtkosten:")
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), app.Budget.TotalCost)
row++
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), "Foerderbetrag:")
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), app.Budget.RequestedFunding)
row++
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), "Eigenanteil:")
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), app.Budget.OwnContribution)
}
// Set column widths
f.SetColWidth(sheetName, "A", "A", 6)
f.SetColWidth(sheetName, "B", "B", 15)
f.SetColWidth(sheetName, "C", "C", 35)
f.SetColWidth(sheetName, "D", "D", 15)
f.SetColWidth(sheetName, "E", "E", 8)
f.SetColWidth(sheetName, "F", "F", 12)
f.SetColWidth(sheetName, "G", "G", 12)
f.SetColWidth(sheetName, "H", "H", 12)
f.SetColWidth(sheetName, "I", "I", 15)
// Add currency format
currencyStyle, _ := f.NewStyle(&excelize.Style{
NumFmt: 44, // Currency format
})
f.SetColStyle(sheetName, "F", currencyStyle)
f.SetColStyle(sheetName, "G", currencyStyle)
var buf bytes.Buffer
if err := f.Write(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// GenerateDataProtectionConcept generates the data protection concept as PDF
func (s *ExportService) GenerateDataProtectionConcept(app *FundingApplication) ([]byte, error) {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.SetMargins(25, 25, 25)
pdf.AddPage()
// Header
pdf.SetFont("Helvetica", "B", 14)
pdf.Cell(0, 10, "Datenschutz- und Betriebskonzept")
pdf.Ln(15)
pdf.SetFont("Helvetica", "", 10)
pdf.Cell(0, 6, fmt.Sprintf("Antragsnummer: %s", app.ApplicationNumber))
pdf.Ln(6)
if app.SchoolProfile != nil {
pdf.Cell(0, 6, fmt.Sprintf("Schule: %s", app.SchoolProfile.Name))
}
pdf.Ln(15)
// Section: Lokale Verarbeitung
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "1. Grundsaetze der Datenverarbeitung")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.ProjectPlan != nil && app.ProjectPlan.DataProtection != "" {
pdf.MultiCell(0, 6, app.ProjectPlan.DataProtection, "", "", false)
} else {
pdf.MultiCell(0, 6, "Das Projekt setzt auf eine vollstaendig lokale Datenverarbeitung:\n\n"+
"- Alle Daten werden ausschliesslich auf den schuleigenen Systemen verarbeitet\n"+
"- Keine Uebermittlung personenbezogener Daten an externe Dienste\n"+
"- Keine Cloud-Speicherung sensibler Daten\n"+
"- Betrieb im Verantwortungsbereich der Schule", "", "", false)
}
pdf.Ln(10)
// Section: Technische Massnahmen
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "2. Technische und organisatorische Massnahmen")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
pdf.MultiCell(0, 6, "Folgende TOMs werden umgesetzt:\n\n"+
"- Zugriffskontrolle ueber schuleigene Benutzerverwaltung\n"+
"- Verschluesselte Datenspeicherung\n"+
"- Regelmaessige Sicherheitsupdates\n"+
"- Protokollierung von Zugriffen\n"+
"- Automatische Loeschung nach definierten Fristen", "", "", false)
pdf.Ln(10)
// Section: Betriebskonzept
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "3. Betriebskonzept")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.ProjectPlan != nil && app.ProjectPlan.MaintenancePlan != "" {
pdf.MultiCell(0, 6, app.ProjectPlan.MaintenancePlan, "", "", false)
} else {
pdf.MultiCell(0, 6, "Der laufende Betrieb wird wie folgt sichergestellt:\n\n"+
"- Schulung des technischen Personals\n"+
"- Dokumentierte Betriebsverfahren\n"+
"- Regelmaessige Wartung und Updates\n"+
"- Definierte Ansprechpartner", "", "", false)
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// GenerateExportBundle generates a ZIP file with all documents
func (s *ExportService) GenerateExportBundle(app *FundingApplication) ([]byte, error) {
var buf bytes.Buffer
zipWriter := zip.NewWriter(&buf)
// Generate and add application letter
letter, err := s.GenerateApplicationLetter(app)
if err == nil {
w, _ := zipWriter.Create(fmt.Sprintf("%s_Antragsschreiben.pdf", app.ApplicationNumber))
w.Write(letter)
}
// Generate and add budget plan
budget, err := s.GenerateBudgetPlan(app)
if err == nil {
w, _ := zipWriter.Create(fmt.Sprintf("%s_Kostenplan.xlsx", app.ApplicationNumber))
w.Write(budget)
}
// Generate and add data protection concept
dp, err := s.GenerateDataProtectionConcept(app)
if err == nil {
w, _ := zipWriter.Create(fmt.Sprintf("%s_Datenschutzkonzept.pdf", app.ApplicationNumber))
w.Write(dp)
}
// Add attachments
for _, attachment := range app.Attachments {
// Read attachment from storage and add to ZIP
// This would need actual file system access
_ = attachment
}
if err := zipWriter.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// ExportDocument represents a generated document
type GeneratedDocument struct {
Name string
Type string // pdf, xlsx, docx
Content []byte
MimeType string
}
// GenerateAllDocuments generates all documents for an application
func (s *ExportService) GenerateAllDocuments(app *FundingApplication) ([]GeneratedDocument, error) {
var docs []GeneratedDocument
// Application letter
letter, err := s.GenerateApplicationLetter(app)
if err == nil {
docs = append(docs, GeneratedDocument{
Name: fmt.Sprintf("%s_Antragsschreiben.pdf", app.ApplicationNumber),
Type: "pdf",
Content: letter,
MimeType: "application/pdf",
})
}
// Budget plan
budget, err := s.GenerateBudgetPlan(app)
if err == nil {
docs = append(docs, GeneratedDocument{
Name: fmt.Sprintf("%s_Kostenplan.xlsx", app.ApplicationNumber),
Type: "xlsx",
Content: budget,
MimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
})
}
// Data protection concept
dp, err := s.GenerateDataProtectionConcept(app)
if err == nil {
docs = append(docs, GeneratedDocument{
Name: fmt.Sprintf("%s_Datenschutzkonzept.pdf", app.ApplicationNumber),
Type: "pdf",
Content: dp,
MimeType: "application/pdf",
})
}
return docs, nil
}
// WriteZipToWriter writes all documents to a zip writer
func (s *ExportService) WriteZipToWriter(app *FundingApplication, w io.Writer) error {
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
docs, err := s.GenerateAllDocuments(app)
if err != nil {
return err
}
for _, doc := range docs {
f, err := zipWriter.Create(doc.Name)
if err != nil {
continue
}
f.Write(doc.Content)
}
return nil
}