v0.3.0 — PDF Export + OTA Download
This commit is contained in:
275
pdf_export.go
Normal file
275
pdf_export.go
Normal file
@@ -0,0 +1,275 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user