From 2eab5983486c72b1855e6fe807ccbfe95f75da0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Mon, 8 Sep 2025 00:32:29 +0200 Subject: [PATCH] working on printable PDF Forms --- Backend/endpoints/pdf.go | 29 +++++ Backend/endpoints/team.go | 2 +- Backend/endpoints/team_presence.go | 2 +- Backend/endpoints/time.go | 6 +- Backend/helper/strings.go | 9 ++ Backend/main.go | 1 + Backend/models/user.go | 2 +- Backend/models/workDay.go | 4 +- Backend/models/workWeek.go | 2 +- Backend/static/css/styles.css | 49 +++++--- Backend/templates/pdf.templ | 46 ++++++++ Backend/templates/pdf_templ.go | 175 +++++++++++++++++++++++++++++ 12 files changed, 305 insertions(+), 22 deletions(-) create mode 100644 Backend/endpoints/pdf.go create mode 100644 Backend/helper/strings.go create mode 100644 Backend/templates/pdf.templ create mode 100644 Backend/templates/pdf_templ.go diff --git a/Backend/endpoints/pdf.go b/Backend/endpoints/pdf.go new file mode 100644 index 0000000..0e47fcd --- /dev/null +++ b/Backend/endpoints/pdf.go @@ -0,0 +1,29 @@ +package endpoints + +import ( + "arbeitszeitmessung/helper" + "arbeitszeitmessung/models" + "arbeitszeitmessung/templates" + "log" + "net/http" + "time" +) + +func PDFHandler(w http.ResponseWriter, r *http.Request) { + helper.RequiresLogin(Session, w, r) + startDate, err := time.Parse("2006-01-02", "2025-08-01") + if err != nil { + log.Println("Error") + } + endDate := startDate.AddDate(0, 1, -1) + + user, err := models.GetUserFromSession(Session, r.Context()) + if err != nil { + log.Println("Error getting user!") + } + + weeks := models.GetWorkDays(user.CardUID, startDate, endDate) + + log.Printf("Using Dates: %s - %s\n", startDate.String(), endDate.String()) + templates.PDFReportEmploye(user, weeks, startDate, endDate).Render(r.Context(), w) +} diff --git a/Backend/endpoints/team.go b/Backend/endpoints/team.go index 33c5569..397d45a 100644 --- a/Backend/endpoints/team.go +++ b/Backend/endpoints/team.go @@ -56,7 +56,7 @@ func submitReport(w http.ResponseWriter, r *http.Request) { } func showWeeks(w http.ResponseWriter, r *http.Request) { - user, err := (*models.User).GetUserFromSession(nil, Session, r.Context()) + user, err := models.GetUserFromSession(Session, r.Context()) if err != nil { log.Println("No user found with the given personal number!") http.Redirect(w, r, "/user/login", http.StatusSeeOther) diff --git a/Backend/endpoints/team_presence.go b/Backend/endpoints/team_presence.go index b313627..e4ea775 100644 --- a/Backend/endpoints/team_presence.go +++ b/Backend/endpoints/team_presence.go @@ -23,7 +23,7 @@ func TeamPresenceHandler(w http.ResponseWriter, r *http.Request) { } func teamPresence(w http.ResponseWriter, r *http.Request) { - user, err := (*models.User).GetUserFromSession(nil, Session, r.Context()) + user, err := models.GetUserFromSession(Session, r.Context()) if err != nil { log.Println("Error getting user!", err) } diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index ac930e7..93b2b74 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -44,7 +44,7 @@ func parseTimestamp(r *http.Request, getKey string, fallback string) (time.Time, // Returns bookings from DB with similar card uid -> checks for card uid in http query params func getBookings(w http.ResponseWriter, r *http.Request) { - user, err := (*models.User).GetUserFromSession(nil, Session, r.Context()) + user, err := models.GetUserFromSession(Session, r.Context()) if err != nil { log.Println("No user found with the given personal number!") http.Redirect(w, r, "/user/login", http.StatusSeeOther) @@ -66,7 +66,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) { } tsTo = tsTo.AddDate(0, 0, 1) // so that today is inside - workDays := (*models.WorkDay).GetWorkDays(nil, user.CardUID, tsFrom, tsTo) + workDays := models.GetWorkDays(user.CardUID, tsFrom, tsTo) sort.Slice(workDays, func(i, j int) bool { return workDays[i].Day.After(workDays[j].Day) }) @@ -104,7 +104,7 @@ func updateBooking(w http.ResponseWriter, r *http.Request) { log.Println("Error loading location", err) loc = time.Local } - user, err := (*models.User).GetUserFromSession(nil, Session, r.Context()) + user, err := models.GetUserFromSession(Session, r.Context()) if err != nil { log.Println("No user found!", err) return diff --git a/Backend/helper/strings.go b/Backend/helper/strings.go new file mode 100644 index 0000000..4a15702 --- /dev/null +++ b/Backend/helper/strings.go @@ -0,0 +1,9 @@ +package helper + +func GetFirst[T, U any](val T, _ U) T { + return val +} + +func GetSecond[T, U any](_ T, val U) U { + return val +} diff --git a/Backend/main.go b/Backend/main.go index 272eb6f..98a5e22 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -50,6 +50,7 @@ func main() { // server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) server.HandleFunc("/team", endpoints.TeamHandler) server.HandleFunc("/team/presence", endpoints.TeamPresenceHandler) + server.HandleFunc("/pdf", endpoints.PDFHandler) server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect)) server.Handle("/static/", http.StripPrefix("/static/", fs)) diff --git a/Backend/models/user.go b/Backend/models/user.go index 6e82c70..016d683 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -22,7 +22,7 @@ type User struct { Overtime time.Duration } -func (u *User) GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) { +func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) { var user User var err error if helper.GetEnv("GO_ENV", "production") == "debug" { diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index 54f23b8..31da794 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -19,7 +19,7 @@ type WorkDay struct { Absence Absence } -func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay { +func GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay { var workDays []WorkDay var workSec, pauseSec float64 @@ -181,7 +181,7 @@ func (d *WorkDay) getWorkTime() { d.calcPauseTime() } -func (d *WorkDay) GetWorkTimeString() (string, string) { +func (d *WorkDay) GetWorkTimeString() (work string, pause string) { workString := helper.FormatDuration(d.workTime) pauseString := helper.FormatDuration(d.pauseTime) return workString, pauseString diff --git a/Backend/models/workWeek.go b/Backend/models/workWeek.go index 12c7a78..cb473d2 100644 --- a/Backend/models/workWeek.go +++ b/Backend/models/workWeek.go @@ -45,7 +45,7 @@ func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek { } func (w *WorkWeek) PopulateWithBookings(worktime time.Duration, overtime time.Duration) { - w.WorkDays = (*WorkDay).GetWorkDays(nil, w.User.CardUID, w.WeekStart, w.WeekStart.Add(7*24*time.Hour)) + w.WorkDays = GetWorkDays(w.User.CardUID, w.WeekStart, w.WeekStart.Add(7*24*time.Hour)) if absences, err := GetAbsencesByCardUID(w.User.CardUID, w.WeekStart, w.WeekStart.Add(7*24*time.Hour)); err == nil { w.Absences = absences } else { diff --git a/Backend/static/css/styles.css b/Backend/static/css/styles.css index de26b86..97a4852 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -189,6 +189,9 @@ .\@container { container-type: inline-size; } + .relative { + position: relative; + } .col-span-2 { grid-column: span 2 / span 2; } @@ -364,6 +367,30 @@ .grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); } + .grid-cols-6 { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + .grid-cols-7 { + grid-template-columns: repeat(7, minmax(0, 1fr)); + } + .grid-cols-\[1fr\] { + grid-template-columns: 1fr; + } + .grid-cols-\[1fr_1fr\] { + grid-template-columns: 1fr 1fr; + } + .grid-cols-\[1fr_1fr_1fr_1fr_1fr_1fr\] { + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; + } + .grid-cols-\[auto_1fr_1fr_1fr_1fr_1fr\] { + grid-template-columns: auto 1fr 1fr 1fr 1fr 1fr; + } + .grid-cols-\[auto_1fr_1fr_1fr_1fr_1fr_1fr\] { + grid-template-columns: auto 1fr 1fr 1fr 1fr 1fr 1fr; + } + .grid-rows-6 { + grid-template-rows: repeat(6, minmax(0, 1fr)); + } .flex-col { flex-direction: column; } @@ -427,10 +454,6 @@ border-style: var(--tw-border-style); border-width: 1px; } - .border-0 { - border-style: var(--tw-border-style); - border-width: 0px; - } .border-neutral-200 { border-color: var(--color-neutral-200); } @@ -452,9 +475,6 @@ .bg-neutral-400 { background-color: var(--color-neutral-400); } - .bg-neutral-700 { - background-color: var(--color-neutral-700); - } .bg-orange-500 { background-color: var(--color-orange-500); } @@ -473,6 +493,15 @@ .p-2 { padding: calc(var(--spacing) * 2); } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .p-8 { + padding: calc(var(--spacing) * 8); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } .px-3 { padding-inline: calc(var(--spacing) * 3); } @@ -699,12 +728,6 @@ } } } - .max-md\:border-0 { - @media (width < 48rem) { - border-style: var(--tw-border-style); - border-width: 0px; - } - } .max-md\:border-b-1 { @media (width < 48rem) { border-bottom-style: var(--tw-border-style); diff --git a/Backend/templates/pdf.templ b/Backend/templates/pdf.templ new file mode 100644 index 0000000..2e87875 --- /dev/null +++ b/Backend/templates/pdf.templ @@ -0,0 +1,46 @@ +package templates + +import ( + "arbeitszeitmessung/helper" + "arbeitszeitmessung/models" + "time" +) + +templ PDFReportEmploye(e models.User, workDays []models.WorkDay, tsStart time.Time, tsEnd time.Time) { + {{ + _, kw := tsStart.ISOWeek() + }} + @Base() + +
+

Kim Mustermensch

+

Zeitraum: { tsStart.Format("02.01.2006") } - { tsEnd.Format("02.01.2006") }

+

Arbeitszeit:

+

Überstunden:

+
+
+

KW

+

Montag

+

Dienstag

+

Mittwoche

+

Donnerstag

+

Freitag

+ //

Samstag

+

{ kw }

+ for d := time.Monday; d < tsStart.Weekday(); d++ { +

+ } + for _, day := range workDays { + if day.Day.Weekday() == time.Monday { +

{ helper.GetKW(day.Day) }

+ } +
+

{ helper.GetFirst(day.GetWorkTimeString()) }

+ for i := 0; i < len(day.Bookings); i += 2 { +

{ day.Bookings[i].Timestamp.Format("15:04") } - { day.Bookings[i+1].Timestamp.Format("15:04") }

+ } +
+ } +
+
+} diff --git a/Backend/templates/pdf_templ.go b/Backend/templates/pdf_templ.go new file mode 100644 index 0000000..3ebeaa7 --- /dev/null +++ b/Backend/templates/pdf_templ.go @@ -0,0 +1,175 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.924 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "arbeitszeitmessung/helper" + "arbeitszeitmessung/models" + "time" +) + +func PDFReportEmploye(e models.User, workDays []models.WorkDay, tsStart time.Time, tsEnd time.Time) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + + _, kw := tsStart.ISOWeek() + templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Kim Mustermensch

Zeitraum: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(tsStart.Format("02.01.2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 17, Col: 52} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(tsEnd.Format("02.01.2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 17, Col: 98} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

Arbeitszeit:

Überstunden:

KW

Montag

Dienstag

Mittwoche

Donnerstag

Freitag

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(kw) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 29, Col: 10} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for d := time.Monday; d < tsStart.Weekday(); d++ { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for _, day := range workDays { + if day.Day.Weekday() == time.Monday { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(helper.GetKW(day.Day)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 35, Col: 31} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(helper.GetFirst(day.GetWorkTimeString())) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 38, Col: 50} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for i := 0; i < len(day.Bookings); i += 2 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(day.Bookings[i].Timestamp.Format("15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 40, Col: 52} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " - ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(day.Bookings[i+1].Timestamp.Format("15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 40, Col: 102} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate