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 }