Mobile-Update + Bericht
release-tag / release-image (push) Successful in 2m6s

This commit is contained in:
2026-08-14 18:17:38 +02:00
parent 2d384046dd
commit 57f7470310
9 changed files with 608 additions and 16 deletions
+212
View File
@@ -124,6 +124,218 @@ func makePDF(entries []Entry, cfg Settings, owner User, compact bool) []byte {
return buildSimplePDF(pageW, pageH, pages, cfg.ExportDate, loc)
}
type serviceReportMeta struct {
Client string
Contact string
Location string
OrderNumber string
Subject string
Notes string
Place string
ReportDate string
}
func makeServiceReportPDF(entries []Entry, cfg Settings, owner User, meta serviceReportMeta) []byte {
loc := location(cfg.Timezone)
technician := strings.TrimSpace(cfg.ExportName)
if technician == "" {
technician = strings.TrimSpace(owner.DisplayName)
}
if technician == "" {
technician = owner.Username
}
if strings.TrimSpace(meta.Subject) == "" {
meta.Subject = "Service-Bericht"
}
if strings.TrimSpace(meta.ReportDate) == "" {
meta.ReportDate = time.Now().In(loc).Format("02.01.2006")
}
const pageW, pageH = 595.0, 842.0
var pages [][]pdfPageLine
var page []pdfPageLine
y := 795.0
add := func(x, yy, size float64, bold bool, text string) {
page = append(page, pdfPageLine{x, yy, size, bold, text})
}
addTableHeader := func() {
add(50, y, 8.5, true, "Datum")
add(108, y, 8.5, true, "Zeit")
add(184, y, 8.5, true, "Dauer")
add(244, y, 8.5, true, "Tätigkeit / Leistung")
y -= 10
add(50, y, 6.5, false, strings.Repeat("-", 104))
y -= 14
}
newPage := func(continuation, includeTable bool) {
if len(page) > 0 {
pages = append(pages, page)
}
page = []pdfPageLine{}
y = 795
title := "SERVICE-BERICHT"
if continuation {
title += " - FORTSETZUNG"
}
add(50, y, 18, true, title)
if strings.TrimSpace(meta.OrderNumber) != "" {
add(405, y+2, 8.5, true, "Auftrag / Ticket")
add(405, y-11, 9.5, false, clipRunes(meta.OrderNumber, 25))
}
y -= 30
if !continuation {
add(50, y, 8, true, "Kunde")
add(50, y-14, 10, false, clipRunes(meta.Client, 48))
add(310, y, 8, true, "Ansprechpartner")
add(310, y-14, 10, false, clipRunes(meta.Contact, 39))
y -= 42
add(50, y, 8, true, "Einsatzort")
add(50, y-14, 9.5, false, clipRunes(meta.Location, 48))
add(310, y, 8, true, "Berichtsdatum")
add(310, y-14, 9.5, false, meta.ReportDate)
y -= 42
add(50, y, 8, true, "Techniker")
add(50, y-14, 9.5, false, clipRunes(technician, 48))
add(310, y, 8, true, "Betreff")
add(310, y-14, 9.5, false, clipRunes(meta.Subject, 39))
y -= 38
}
if includeTable {
addTableHeader()
}
}
newPage(false, true)
var total int64
for _, e := range entries {
if e.EndMS == nil {
continue
}
start := time.UnixMilli(e.StartMS).In(loc)
end := time.UnixMilli(*e.EndMS).In(loc)
d := roundedDuration(*e.EndMS-e.StartMS, cfg.RoundingMinutes, cfg.RoundUp)
total += d
activity := strings.TrimSpace(e.Activity)
if activity == "" {
activity = "-"
}
activityLines := wrapPDFText(activity, 50)
rowLines := len(activityLines)
if rowLines < 1 {
rowLines = 1
}
rowHeight := float64(rowLines*12 + 10)
if y-rowHeight < 105 {
newPage(true, true)
}
add(50, y, 8.5, false, start.Format("02.01.06"))
add(108, y, 8.5, false, formatClock(start, cfg.TimeFormat)+"-"+formatClock(end, cfg.TimeFormat))
add(184, y, 8.5, false, formatDuration(d))
for i, line := range activityLines {
add(244, y-float64(i*12), 8.5, false, line)
}
y -= rowHeight - 4
add(50, y, 6, false, strings.Repeat("-", 104))
y -= 10
}
if y < 155 {
newPage(true, false)
}
add(392, y, 10, true, "Gesamtdauer")
add(493, y, 10, true, formatDuration(total))
y -= 30
if notes := strings.TrimSpace(meta.Notes); notes != "" {
if y < 145 {
newPage(true, false)
}
add(50, y, 9, true, "Zusammenfassung / Bemerkungen")
y -= 16
for _, line := range wrapPDFText(notes, 86) {
if y < 130 {
newPage(true, false)
}
add(50, y, 9, false, line)
y -= 13
}
y -= 12
}
// Reserve enough room so both signatures always stay together.
if y < 175 {
newPage(true, false)
}
add(50, y, 8.5, false, "Die aufgeführten Leistungen und Zeiten wurden erbracht und zur Kenntnis genommen.")
y -= 25
placeDate := strings.TrimSpace(meta.Place)
if placeDate != "" {
placeDate += ", "
}
placeDate += meta.ReportDate
add(50, y, 8, true, "Ort / Datum")
add(117, y, 8.5, false, clipRunes(placeDate, 65))
y -= 47
add(50, y, 9, false, "____________________________________")
add(330, y, 9, false, "____________________________________")
y -= 14
add(50, y, 8, true, "Unterschrift Kunde")
add(330, y, 8, true, "Unterschrift Techniker")
y -= 13
add(50, y, 7.5, false, clipRunes(meta.Contact, 40))
add(330, y, 7.5, false, clipRunes(technician, 40))
pages = append(pages, page)
return buildSimplePDF(pageW, pageH, pages, cfg.ExportDate, loc)
}
func wrapPDFText(s string, max int) []string {
if max < 1 {
max = 1
}
s = strings.ReplaceAll(s, "\r\n", "\n")
s = strings.ReplaceAll(s, "\r", "\n")
var out []string
for _, paragraph := range strings.Split(s, "\n") {
paragraph = strings.TrimSpace(paragraph)
if paragraph == "" {
out = append(out, "")
continue
}
words := strings.Fields(paragraph)
line := ""
for _, word := range words {
for len([]rune(word)) > max {
if line != "" {
out = append(out, line)
line = ""
}
r := []rune(word)
out = append(out, string(r[:max]))
word = string(r[max:])
}
candidate := word
if line != "" {
candidate = line + " " + word
}
if len([]rune(candidate)) > max && line != "" {
out = append(out, line)
line = word
} else {
line = candidate
}
}
if line != "" {
out = append(out, line)
}
}
if len(out) == 0 {
return []string{""}
}
return out
}
// buildSimplePDF writes a small, standards-compliant PDF using only built-in Type 1
// fonts. This avoids pulling a PDF framework into the server binary.
func buildSimplePDF(pageW, pageH float64, pages [][]pdfPageLine, exportDate bool, loc *time.Location) []byte {