Files
speiseplan/pdf_export.go
2026-02-20 10:17:09 +00:00

276 lines
6.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"fmt"
"strings"
"time"
"github.com/go-pdf/fpdf"
)
// ExportPDF exportiert einen Wochenplan als PDF im Querformat A4
func (a *App) ExportPDF(weekPlanID int, outputPath string) error {
// Plan aus DB laden
var plan WeekPlan
err := db.Get(&plan, "SELECT id, year, week, created_at FROM week_plans WHERE id = ?", weekPlanID)
if err != nil {
return fmt.Errorf("Wochenplan nicht gefunden: %w", err)
}
entries, err := a.loadPlanEntries(plan.ID)
if err != nil {
return err
}
plan.Entries = entries
specialDays, err := a.loadSpecialDays(plan.ID)
if err != nil {
return err
}
plan.SpecialDays = specialDays
// Montag der KW berechnen
monday := isoWeekMonday(plan.Year, plan.Week)
friday := monday.AddDate(0, 0, 4)
// PDF erstellen - Querformat A4
pdf := fpdf.New("L", "mm", "A4", "")
pdf.SetAutoPageBreak(false, 0)
// UTF-8 Font für deutsche Umlaute
pdf.AddUTF8Font("DejaVu", "", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
pdf.AddUTF8Font("DejaVu", "B", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf")
pdf.AddPage()
pageW, pageH := pdf.GetPageSize()
marginX := 10.0
usableW := pageW - 2*marginX
// === ÜBERSCHRIFT ===
pdf.SetFont("DejaVu", "B", 16)
title := fmt.Sprintf("Wochenspeiseplan KW %d / %d", plan.Week, plan.Year)
pdf.CellFormat(usableW, 10, title, "", 0, "C", false, 0, "")
pdf.Ln(7)
pdf.SetFont("DejaVu", "", 10)
dateRange := fmt.Sprintf("%s %s", monday.Format("02.01.2006"), friday.Format("02.01.2006"))
pdf.CellFormat(usableW, 6, dateRange, "", 0, "C", false, 0, "")
pdf.Ln(10)
tableTop := pdf.GetY()
// === TABELLE ===
colW := usableW / 5.0
dayNames := []string{"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"}
// Sondertage-Map
specialMap := map[int]SpecialDay{}
for _, sd := range plan.SpecialDays {
specialMap[sd.Day] = sd
}
// Einträge nach Tag+Meal gruppieren
type mealEntries struct {
fruehstueck []PlanEntry
vesper []PlanEntry
}
dayEntries := map[int]*mealEntries{}
for i := 1; i <= 5; i++ {
dayEntries[i] = &mealEntries{}
}
for _, e := range plan.Entries {
me := dayEntries[e.Day]
if me == nil {
continue
}
if e.Meal == "fruehstueck" {
me.fruehstueck = append(me.fruehstueck, e)
} else {
me.vesper = append(me.vesper, e)
}
}
// Allergene & Zusatzstoffe sammeln
allergenSet := map[string]Allergen{}
additiveSet := map[string]Additive{}
for _, e := range plan.Entries {
if e.Product != nil {
for _, al := range e.Product.Allergens {
allergenSet[al.ID] = al
}
for _, ad := range e.Product.Additives {
additiveSet[ad.ID] = ad
}
}
}
// Kopfzeile: Tage
headerH := 12.0
pdf.SetFont("DejaVu", "B", 11)
pdf.SetFillColor(220, 220, 220)
for i, name := range dayNames {
d := monday.AddDate(0, 0, i)
label := fmt.Sprintf("%s, %s", name, d.Format("02.01."))
x := marginX + float64(i)*colW
pdf.SetXY(x, tableTop)
pdf.CellFormat(colW, headerH, label, "1", 0, "C", true, 0, "")
}
pdf.Ln(-1)
// Zeilen: Frühstück + Vesper
mealLabels := []string{"Frühstück", "Vesper"}
mealKeys := []string{"fruehstueck", "vesper"}
// Berechne verfügbare Höhe für die 2 Mahlzeit-Zeilen
legendEstimate := 30.0 // Platz für Legende + Footer
availH := pageH - tableTop - headerH - legendEstimate
rowH := availH / 2.0
if rowH > 60 {
rowH = 60
}
for mi, mealKey := range mealKeys {
rowTop := tableTop + headerH + float64(mi)*rowH
for day := 1; day <= 5; day++ {
x := marginX + float64(day-1)*colW
// Sondertag?
if sd, ok := specialMap[day]; ok {
pdf.SetFillColor(200, 200, 200)
pdf.SetXY(x, rowTop)
pdf.SetFont("DejaVu", "B", 10)
label := sd.Type
if sd.Label != nil && *sd.Label != "" {
label = *sd.Label
}
if mi == 0 {
pdf.CellFormat(colW, rowH, label, "1", 0, "C", true, 0, "")
} else {
pdf.CellFormat(colW, rowH, "", "1", 0, "C", true, 0, "")
}
continue
}
// Meal-Label
pdf.SetFillColor(245, 245, 245)
pdf.SetXY(x, rowTop)
pdf.SetFont("DejaVu", "B", 9)
pdf.CellFormat(colW, 6, mealLabels[mi], "LTR", 0, "C", true, 0, "")
// Einträge
me := dayEntries[day]
var items []PlanEntry
if mealKey == "fruehstueck" {
items = me.fruehstueck
} else {
items = me.vesper
}
pdf.SetFont("DejaVu", "", 9)
contentTop := rowTop + 6
contentH := rowH - 6
pdf.SetXY(x+1, contentTop)
for _, item := range items {
text := formatEntryText(item)
pdf.SetX(x + 1)
pdf.MultiCell(colW-2, 4, text, "", "L", false)
}
// Rahmen um die ganze Zelle
pdf.Rect(x, rowTop, colW, rowH, "D")
_ = contentH
}
}
// === LEGENDE ===
legendY := tableTop + headerH + 2*rowH + 3
pdf.SetXY(marginX, legendY)
if len(allergenSet) > 0 {
pdf.SetFont("DejaVu", "B", 8)
pdf.CellFormat(0, 4, "Allergene:", "", 0, "L", false, 0, "")
pdf.Ln(4)
pdf.SetFont("DejaVu", "", 7)
var parts []string
for id, al := range allergenSet {
parts = append(parts, fmt.Sprintf("%s = %s", id, al.Name))
}
pdf.MultiCell(usableW, 3.5, strings.Join(parts, " | "), "", "L", false)
pdf.Ln(1)
}
if len(additiveSet) > 0 {
pdf.SetFont("DejaVu", "B", 8)
pdf.CellFormat(0, 4, "Zusatzstoffe:", "", 0, "L", false, 0, "")
pdf.Ln(4)
pdf.SetFont("DejaVu", "", 7)
var parts []string
for id, ad := range additiveSet {
parts = append(parts, fmt.Sprintf("%s = %s", id, ad.Name))
}
pdf.MultiCell(usableW, 3.5, strings.Join(parts, " | "), "", "L", false)
}
// === FOOTER ===
pdf.SetFont("DejaVu", "", 8)
footerY := pageH - 8
pdf.SetXY(marginX, footerY)
pdf.CellFormat(usableW/2, 5, fmt.Sprintf("Erstellt am %s", time.Now().Format("02.01.2006")), "", 0, "L", false, 0, "")
pdf.CellFormat(usableW/2, 5, "Seite 1", "", 0, "R", false, 0, "")
return pdf.OutputFileAndClose(outputPath)
}
// formatEntryText formatiert einen PlanEntry für die PDF-Ausgabe
func formatEntryText(e PlanEntry) string {
var text string
if e.Product != nil {
text = e.Product.Name
// Allergen/Zusatzstoff-Kürzel anhängen
var codes []string
for _, al := range e.Product.Allergens {
codes = append(codes, al.ID)
}
for _, ad := range e.Product.Additives {
codes = append(codes, ad.ID)
}
if len(codes) > 0 {
text += " (" + strings.Join(codes, ",") + ")"
}
// Multiline: Inhaltsstoffe auf eigener Zeile
if e.Product.Multiline && len(codes) > 0 {
text = e.Product.Name + "\n [" + strings.Join(codes, ",") + "]"
}
} else if e.CustomText != nil {
text = *e.CustomText
}
// Gruppenlabel
if e.GroupLabel != nil && *e.GroupLabel != "" {
text = fmt.Sprintf("[%s] %s", *e.GroupLabel, text)
}
return text
}
// isoWeekMonday gibt den Montag der ISO-Kalenderwoche zurück
func isoWeekMonday(year, week int) time.Time {
// 4. Januar liegt immer in KW 1
jan4 := time.Date(year, 1, 4, 0, 0, 0, 0, time.Local)
// Wochentag von 4. Jan (0=So, 1=Mo, ...)
weekday := int(jan4.Weekday())
if weekday == 0 {
weekday = 7
}
// Montag der KW 1
mondayKW1 := jan4.AddDate(0, 0, -(weekday - 1))
// Montag der gewünschten KW
return mondayKW1.AddDate(0, 0, (week-1)*7)
}