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>
396 lines
12 KiB
Go
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
|
|
}
|