From 46218f9bcaab488c3443584449449e272a98baca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 12 Feb 2026 16:46:57 +0100 Subject: [PATCH] fix: weekbased calculation pdf report with this change the time calculations for pdf reports should be better line with the reports send as "week_report" --- Backend/endpoints/pdf-create.go | 235 ++++++++++++++++++----------- Backend/endpoints/team-report.go | 4 +- Backend/endpoints/time.go | 2 +- Backend/helper/system.go | 4 + Backend/helper/time.go | 59 ++++++++ Backend/helper/time_test.go | 95 ++++++++++++ Backend/helper/web.go | 2 +- Backend/main.go | 2 +- Backend/models/absence.go | 2 +- Backend/models/booking.go | 2 +- Backend/models/iworkday.go | 4 +- Backend/models/publicHoliday.go | 2 + Backend/models/user.go | 8 +- Backend/models/workDay.go | 13 +- Backend/models/workWeek.go | 37 ++++- Backend/models/workWeek_test.go | 2 +- Backend/static/css/styles.css | 68 --------- Backend/templates/reportPage.templ | 2 +- 18 files changed, 365 insertions(+), 178 deletions(-) diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index b93be94..01efb09 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -21,66 +21,7 @@ import ( const DE_DATE string = "02.01.2006" const FILE_YEAR_MONTH string = "2006_01" -func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) { - var typstDays []typstDay - for _, day := range days { - var thisTypstDay typstDay - work, pause, overtime := day.GetTimes(u, models.WorktimeBaseDay, false) - workVirtual := day.GetWorktime(u, models.WorktimeBaseDay, true) - overtime = workVirtual - u.ArbeitszeitProWocheFrac(0.2) - thisTypstDay.Date = day.Date().Format(DE_DATE) - thisTypstDay.Worktime = helper.FormatDurationFill(workVirtual, true) - thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) - thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true) - thisTypstDay.IsFriday = day.Date().Weekday() == time.Friday - - if workVirtual > work { - thisTypstDay.Kurzarbeit = helper.FormatDurationFill(workVirtual-work, true) - } else { - thisTypstDay.Kurzarbeit = helper.FormatDurationFill(0, true) - } - - thisTypstDay.DayParts = convertDayToTypstDayParts(day, u) - typstDays = append(typstDays, thisTypstDay) - } - return typstDays, nil -} - -func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDayPart { - var typstDayParts []typstDayPart - switch day.Type() { - case models.DayTypeWorkday: - workDay, _ := day.(*models.WorkDay) - for i := 0; i < len(workDay.Bookings); i += 2 { - var typstDayPart typstDayPart - typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04") - if i+1 < len(workDay.Bookings) { - typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04") - } else { - typstDayPart.BookingTo = workDay.Bookings[i].Timestamp.Format("15:04") - } - typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name - typstDayPart.IsWorkDay = true - typstDayParts = append(typstDayParts, typstDayPart) - } - if day.IsKurzArbeit() && len(workDay.Bookings) > 0 { - tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user) - typstDayParts = append(typstDayParts, typstDayPart{ - BookingFrom: tsFrom.Format("15:04"), - BookingTo: tsTo.Format("15:04"), - WorkType: "Kurzarbeit", - IsWorkDay: true, - }) - } - case models.DayTypeCompound: - for _, c := range day.(*models.CompoundDay).DayParts { - typstDayParts = append(typstDayParts, convertDayToTypstDayParts(c, user)...) - } - default: - typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: day.ToString()}) - } - return typstDayParts -} +const PDF_DIRECTORY = "/home/tom/Code/arbeitszeitmessung/Backend/doc" func PDFCreateController(w http.ResponseWriter, r *http.Request) { helper.RequiresLogin(Session, w, r) @@ -101,14 +42,16 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) { return } - n := 0 - for _, e := range employes { - if user.IsSuperior(e) { - employes[n] = e - n++ + if !helper.IsDebug() { + n := 0 + for _, e := range employes { + if user.IsSuperior(e) { + employes[n] = e + n++ + } } + employes = employes[:n] } - employes = employes[:n] reportData := createReports(employes, startDate) @@ -119,8 +62,9 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) { slog.Warn("Could not create pdf report", slog.Any("Error", err)) w.WriteHeader(http.StatusInternalServerError) } + w.Header().Set("Content-type", "application/pdf") - w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=Monatsabrechnung_%s", startDate.Format(FILE_YEAR_MONTH))) + w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=Monatsabrechnung_%s.pdf", startDate.Format(FILE_YEAR_MONTH))) output.WriteTo(w) w.WriteHeader(http.StatusOK) case "download": @@ -131,11 +75,12 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) { } output, err := zipPfd(pdfReports, &reportData) if err != nil { - slog.Warn("Could not create pdf report", slog.Any("Error", err)) + slog.Warn("Could not zip pdf reports", slog.Any("Error", err)) w.WriteHeader(http.StatusInternalServerError) } + w.Header().Set("Content-type", "application/zip") - w.Header().Set("Content-Disposition", fmt.Sprintf("attachement; filename=Monatsabrechnung_%s", startDate.Format(FILE_YEAR_MONTH))) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachement; filename=Monatsabrechnung_%s.zip", startDate.Format(FILE_YEAR_MONTH))) output.WriteTo(w) w.WriteHeader(http.StatusOK) } @@ -145,6 +90,73 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) { } } +func convertDaysToTypst(days []models.IWorkDay, u models.User, weekbase models.WorktimeBase) ([]typstDay, error) { + var typstDays []typstDay + for i, day := range days { + if !day.IsSubmittedAndAccepted() && !helper.IsDebug() { + continue + } + + var thisTypstDay typstDay + workVirtual, pause, overtime := day.GetTimes(u, weekbase, true) + + if day.Type() != models.DayTypeHoliday { + overtime = workVirtual - u.ArbeitszeitProWocheFrac(0.2) + } + thisTypstDay.Date = day.Date().Format(DE_DATE) + thisTypstDay.Worktime = helper.FormatDurationFill(workVirtual, true) + thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) + thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true) + thisTypstDay.IsFriday = i == len(days)-1 + + if work := day.GetWorktime(u, weekbase, false); workVirtual > work { + thisTypstDay.Kurzarbeit = helper.FormatDurationFill(workVirtual-work, true) + } else { + thisTypstDay.Kurzarbeit = helper.FormatDurationFill(0, true) + } + + thisTypstDay.DayParts = convertDayToTypstDayParts(day, u, weekbase) + typstDays = append(typstDays, thisTypstDay) + } + return typstDays, nil +} + +func convertDayToTypstDayParts(day models.IWorkDay, user models.User, weekBase models.WorktimeBase) []typstDayPart { + var typstDayParts []typstDayPart + switch day.Type() { + case models.DayTypeWorkday: + workDay, _ := day.(*models.WorkDay) + for i := 0; i < len(workDay.Bookings); i += 2 { + var typstDayPart typstDayPart + typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04") + if i+1 < len(workDay.Bookings) { + typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04") + } else { + typstDayPart.BookingTo = workDay.Bookings[i].Timestamp.Format("15:04") + } + typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name + typstDayPart.IsWorkDay = true + typstDayParts = append(typstDayParts, typstDayPart) + } + if day.IsKurzArbeit() { + tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user, weekBase) + typstDayParts = append(typstDayParts, typstDayPart{ + BookingFrom: tsFrom.Format("15:04"), + BookingTo: tsTo.Format("15:04"), + WorkType: "Kurzarbeit", + IsWorkDay: true, + }) + } + case models.DayTypeCompound: + for _, c := range day.(*models.CompoundDay).DayParts { + typstDayParts = append(typstDayParts, convertDayToTypstDayParts(c, user, weekBase)...) + } + default: + typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: day.ToString()}) + } + return typstDayParts +} + func createReports(employes []models.User, startDate time.Time) []typstData { startDate = helper.GetFirstOfMonth(startDate) endDate := startDate.AddDate(0, 1, -1) @@ -161,28 +173,67 @@ func createReports(employes []models.User, startDate time.Time) []typstData { } func createEmployeReport(employee models.User, startDate, endDate time.Time) (typstData, error) { - publicHolidays, err := models.GetHolidaysFromTo(startDate, endDate) + publicHolidays, _ := models.GetHolidaysFromTo(startDate, endDate) targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)-len(publicHolidays)) - workDaysThisMonth := models.GetDays(employee, startDate, endDate.AddDate(0, 0, 1), false) - - slog.Debug("Baseline Working hours", "targetHours", targetHoursThisMonth.Hours()) + daysThisMonth := helper.GenerateDateRange(startDate, endDate) + mondaysThisMonth := helper.GetMondays(daysThisMonth, false) + var weeks []models.WorkWeek var workHours, kurzarbeitHours time.Duration - for _, day := range workDaysThisMonth { - tmpvirtualHours := day.GetWorktime(employee, models.WorktimeBaseDay, true) - tmpactualHours := day.GetWorktime(employee, models.WorktimeBaseDay, false) - if day.IsKurzArbeit() && tmpvirtualHours > tmpactualHours { - slog.Debug("Adding kurzarbeit to workday", "day", day.Date()) - kurzarbeitHours += tmpvirtualHours - tmpactualHours + for _, monday := range mondaysThisMonth { + var week models.WorkWeek + if monday.After(startDate) { + week = models.NewWorkWeekSimple(employee, monday, true) + } else if startDate.Sub(monday) < time.Hour*24*6 { + week = models.NewWorkWeek(employee, startDate, monday.Add(6*24*time.Hour), true) } - workHours += tmpvirtualHours + workHours += week.WorktimeVirtual + kurzarbeitHours += week.WorktimeVirtual - week.Worktime + weeks = append(weeks, week) } + var typstDays []typstDay + for _, week := range weeks { + weekTypstDays, err := convertDaysToTypst(week.Days, employee, week.WeekBase) + if err != nil { + slog.Error("Error converting days into typst", "error", err) + continue + } + typstDays = append(typstDays, weekTypstDays...) + } + + slog.Info("Weeks for the month", "week len", len(weeks), "week", weeks) + // workDaysThisMonth := models.GetDays(employee, startDate, endDate.AddDate(0, 0, 1), false) + + // var weekbase models.WorktimeBase + // if lenWorkDays(workDaysThisMonth) == helper.GetWorkingDays(startDate, endDate) { + // weekbase = models.WorktimeBaseWeek + // } else { + // weekbase = models.WorktimeBaseDay + // } + + // slog.Debug("Baseline Working hours", "targetHours", targetHoursThisMonth.Hours(), "days", helper.GetWorkingDays(startDate, endDate), "workdays", lenWorkDays(workDaysThisMonth)) + + // var workHours, kurzarbeitHours time.Duration + // for _, day := range workDaysThisMonth { + // tmpvirtualHours := day.GetWorktime(employee, weekbase, true) + // tmpactualHours := day.GetWorktime(employee, weekbase, false) + // if day.IsKurzArbeit() && tmpvirtualHours > tmpactualHours { + // slog.Debug("Adding kurzarbeit to workday", "day", day.Date()) + // kurzarbeitHours += tmpvirtualHours - tmpactualHours + // } + // workHours += tmpvirtualHours + // } worktimeBalance := workHours - targetHoursThisMonth - typstDays, err := convertDaysToTypst(workDaysThisMonth, employee) + // typstDays, err := convertDaysToTypst(workDaysThisMonth, employee, weekbase) + // if err != nil { + // slog.Warn("Failed to convert to days", slog.Any("error", err)) + // return typstData{}, err + // } + + totalOvertime, err := employee.GetReportedOvertime(endDate) if err != nil { - slog.Warn("Failed to convert to days", slog.Any("error", err)) - return typstData{}, err + slog.Error("Cannot retrieve total Overtime", "Error", err) } metadata := typstMetadata{ @@ -191,7 +242,7 @@ func createEmployeReport(employee models.User, startDate, endDate time.Time) (ty Overtime: helper.FormatDurationFill(worktimeBalance, true), WorkTime: helper.FormatDurationFill(workHours, true), Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true), - OvertimeTotal: "", + OvertimeTotal: helper.FormatDurationFill(totalOvertime+worktimeBalance, true), CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), } return typstData{Meta: metadata, Days: typstDays, FileName: fmt.Sprintf("%s_%s.pdf", startDate.Format(FILE_YEAR_MONTH), employee.Name)}, nil @@ -202,8 +253,7 @@ func renderPDFSingle(data []typstData) (bytes.Buffer, error) { var output bytes.Buffer typstCLI := typst.CLI{ - WorkingDirectory: "/doc/", - // ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"), + WorkingDirectory: PDF_DIRECTORY, } if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil { @@ -230,8 +280,7 @@ func renderPDFMulti(data []typstData) ([]bytes.Buffer, error) { var outputMulti []bytes.Buffer typstRender := typst.CLI{ - WorkingDirectory: "/doc/", - // ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"), + WorkingDirectory: PDF_DIRECTORY, } for _, d := range data { @@ -273,6 +322,16 @@ func zipPfd(pdfReports []bytes.Buffer, reportData *[]typstData) (bytes.Buffer, e return zipOutput, err } +func lenWorkDays(workDays []models.IWorkDay) int { + var lenght int + for _, day := range workDays { + if !day.IsEmpty() || day.IsKurzArbeit() { + lenght += 1 + } + } + return lenght +} + type typstMetadata struct { TimeRange string `json:"time-range"` EmployeeName string `json:"employee-name"` diff --git a/Backend/endpoints/team-report.go b/Backend/endpoints/team-report.go index 1babc7b..482f8fa 100644 --- a/Backend/endpoints/team-report.go +++ b/Backend/endpoints/team-report.go @@ -42,7 +42,7 @@ func submitReport(w http.ResponseWriter, r *http.Request) { return } - workWeek := models.NewWorkWeek(user, weekTs, true) + workWeek := models.NewWorkWeekSimple(user, weekTs, true) switch r.FormValue("method") { case "send": @@ -70,7 +70,7 @@ func showWeeks(w http.ResponseWriter, r *http.Request) { submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission()) lastSub := helper.GetMonday(submissionDate) - userWeek := models.NewWorkWeek(user, lastSub, true) + userWeek := models.NewWorkWeekSimple(user, lastSub, true) var workWeeks []models.WorkWeek teamMembers, err := user.GetTeamMembers() diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index 09e66f1..883c7b3 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -100,7 +100,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) { } aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, true) } - if reportedOvertime, err := user.GetReportedOvertime(); err == nil { + if reportedOvertime, err := user.GetReportedOvertime(time.Now()); err == nil { user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute) } else { log.Println("Cannot calculate overtime: ", err) diff --git a/Backend/helper/system.go b/Backend/helper/system.go index da3f736..2dfe9a8 100644 --- a/Backend/helper/system.go +++ b/Backend/helper/system.go @@ -20,6 +20,10 @@ func GetEnv(key, fallback string) string { return fallback } +func IsDebug() bool { + return GetEnv("GO_ENV", "production") == "debug" +} + type CacheItem struct { value any expiration time.Time diff --git a/Backend/helper/time.go b/Backend/helper/time.go index 7434a0e..3681ec6 100644 --- a/Backend/helper/time.go +++ b/Backend/helper/time.go @@ -4,6 +4,7 @@ package helper import ( "fmt" + "slices" "time" ) @@ -18,6 +19,64 @@ func GetMonday(ts time.Time) time.Time { return ts } +func GetMondays(allDays []time.Time, onlyInRange bool) []time.Time { + var mondays []time.Time + var start, end time.Time + + for _, day := range allDays { + mondays = append(mondays, GetMonday(day)) + + if start.IsZero() || day.Before(start) { + start = day + } + if end.IsZero() || day.After(end) { + end = day + } + } + mondays = slices.Compact(mondays) + if onlyInRange { + return DaysInRange(mondays, start, end) + } + return mondays +} + +func DaysInRange(days []time.Time, startDate, endDate time.Time) []time.Time { + filtered := []time.Time{} + startDate = startDate.Add(-time.Minute) + endDate = endDate.Add(time.Minute) + + for _, day := range days { + if day.After(startDate) && day.Before(endDate) { + filtered = append(filtered, day) + } + } + return filtered +} + +func IsMonday(day time.Time) bool { + return day.Weekday() == time.Monday +} + +// GenerateDateRange returns a slice of all dates between start and end (inclusive). +func GenerateDateRange(start, end time.Time) []time.Time { + var dates []time.Time + + // Ensure start is before or equal to end + if start.After(end) { + return dates + } + + // Normalize times to midnight + current := start.Truncate(time.Hour * 24) + end = end.Truncate(time.Hour * 24) + + for !current.After(end) { + dates = append(dates, current) + current = current.AddDate(0, 0, 1) // Add one day + } + return dates +} + func GetFirstOfMonth(ts time.Time) time.Time { if ts.Day() > 1 { return ts.AddDate(0, 0, -(ts.Day() - 1)) diff --git a/Backend/helper/time_test.go b/Backend/helper/time_test.go index 7905898..256d3af 100644 --- a/Backend/helper/time_test.go +++ b/Backend/helper/time_test.go @@ -26,6 +26,101 @@ func TestGetMonday(t *testing.T) { } } +func TestIsMonday_ReturnsTrueForMonday(t *testing.T) { + monday := time.Date(2023, 4, 3, 0, 0, 0, 0, time.UTC) + + if !IsMonday(monday) { + t.Errorf("Expected IsMonday to return true for Monday, got false") + } +} + +func TestIsMonday_ReturnsFalseForNonMonday(t *testing.T) { + tuesday := time.Date(2023, 4, 4, 0, 0, 0, 0, time.UTC) + + if IsMonday(tuesday) { + t.Errorf("Expected IsMonday to return false for Tuesday, got true") + } +} + +func TestGenerateDateRange(t *testing.T) { + start := time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC) + end := time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC) + + dates := GenerateDateRange(start, end) + + if len(dates) != 3 { + t.Fatalf("expected 3 dates, got %d", len(dates)) + } + + expected := []string{"2026-02-09", "2026-02-10", "2026-02-11"} + for i, d := range dates { + got := d.Format("2006-01-02") + if got != expected[i] { + t.Errorf("expected %s, got %s", expected[i], got) + } + } +} + +func TestGetMondays_ReturnsOnlyMondays(t *testing.T) { + startDate := time.Date(2026, 01, 01, 0, 0, 0, 0, time.UTC) + endDate := time.Date(2026, 01, 31, 0, 0, 0, 0, time.UTC) + + daysInMonth := GenerateDateRange(startDate, endDate) + result := GetMondays(daysInMonth, false) + if len(result) < 5 { + t.Errorf("Expected 5 monday, got %d", len(result)) + } else if len(result) > 5 { + t.Errorf("Expected 5 monday, got %d", len(result)) + } + + if result[0] != time.Date(2025, 12, 29, 0, 0, 0, 0, time.UTC) { + t.Errorf("Expected first monday to be %v, got %v", "2025-12-29", result[0]) + } +} + +func TestGetMondays_ReturnsOnlyMondaysInRange(t *testing.T) { + startDate := time.Date(2026, 01, 01, 0, 0, 0, 0, time.UTC) + endDate := time.Date(2026, 01, 31, 0, 0, 0, 0, time.UTC) + + daysInMonth := GenerateDateRange(startDate, endDate) + result := GetMondays(daysInMonth, true) + if len(result) < 4 { + t.Errorf("Expected 4 monday, got %d", len(result)) + } else if len(result) > 4 { + t.Errorf("Expected 4 monday, got %d", len(result)) + } + + if result[0] != time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC) { + t.Errorf("Expected first monday to be %v, got %v", "2026-01-05", result[0]) + } +} + +func TestDaysInRange(t *testing.T) { + days := []time.Time{ + time.Date(2023, 4, 3, 0, 0, 0, 0, time.UTC), // Tuesday + time.Date(2023, 4, 4, 0, 0, 0, 0, time.UTC), // Wednesday + time.Date(2023, 4, 5, 0, 0, 0, 0, time.UTC), // Thursday + time.Date(2023, 4, 6, 0, 0, 0, 0, time.UTC), // Friday + } + + start := time.Date(2023, 4, 3, 0, 0, 0, 0, time.UTC) + end := time.Date(2023, 4, 5, 0, 0, 0, 0, time.UTC) + + daysInRange := DaysInRange(days, start, end) + + if len(daysInRange) != 3 { + t.Errorf("Expected 3 days in range, got %d", len(daysInRange)) + } + + if daysInRange[0] != days[0] { + t.Errorf("Expected first day in range to be %v, got %v", days[0], daysInRange[0]) + } + + if daysInRange[2] != days[2] { + t.Errorf("Expected third day in range to be %v, got %v", days[2], daysInRange[2]) + } +} + func TestFormatDurationFill(t *testing.T) { testCases := []struct { name string diff --git a/Backend/helper/web.go b/Backend/helper/web.go index 3fcaadd..cdcc481 100644 --- a/Backend/helper/web.go +++ b/Backend/helper/web.go @@ -24,7 +24,7 @@ func SetCors(w http.ResponseWriter) { func RequiresLogin(session *scs.SessionManager, w http.ResponseWriter, r *http.Request) { r = r.WithContext(context.WithValue(r.Context(), "session", session)) - if GetEnv("GO_ENV", "production") == "debug" { + if IsDebug() { return } if session.Exists(r.Context(), "user") { diff --git a/Backend/main.go b/Backend/main.go index b909882..761cf17 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -38,7 +38,7 @@ func main() { if err != nil { slog.Info("No .env file found in directory!") } - if helper.GetEnv("GO_ENV", "production") == "debug" { + if helper.IsDebug() { logLevel.Set(slog.LevelDebug) envs := os.Environ() slog.Debug("Debug mode enabled", "Environment Variables", envs) diff --git a/Backend/models/absence.go b/Backend/models/absence.go index 21150f6..2246be3 100644 --- a/Backend/models/absence.go +++ b/Backend/models/absence.go @@ -63,7 +63,7 @@ func (a *Absence) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100) case WorktimeBaseWeek: if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit { - return u.ArbeitszeitProTagFrac(0.2) + return u.ArbeitszeitProWocheFrac(0.2) } else if a.AbwesenheitTyp.WorkTime <= 0 { return 0 } diff --git a/Backend/models/booking.go b/Backend/models/booking.go index d406de4..8072a0f 100644 --- a/Backend/models/booking.go +++ b/Backend/models/booking.go @@ -188,7 +188,7 @@ func (b Booking) Save() { } func (b *Booking) GetBookingType() string { - debug := (helper.GetEnv("GO_ENV", "production") == "debug") + debug := helper.IsDebug() switch b.CheckInOut { case 1: //manuelle Änderung return "kommen" diff --git a/Backend/models/iworkday.go b/Backend/models/iworkday.go index 3f96166..53fa920 100644 --- a/Backend/models/iworkday.go +++ b/Backend/models/iworkday.go @@ -55,7 +55,9 @@ func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay } for _, absentDay := range absences { - + if weekDay := absentDay.Date().Weekday(); weekDay == time.Saturday || weekDay == time.Sunday { + continue + } // Check if there is already a day existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)] switch { diff --git a/Backend/models/publicHoliday.go b/Backend/models/publicHoliday.go index bc1b11d..d4fb0d1 100644 --- a/Backend/models/publicHoliday.go +++ b/Backend/models/publicHoliday.go @@ -119,6 +119,8 @@ func (p *PublicHoliday) RequiresAction() bool { } func (p *PublicHoliday) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + return 0 + switch base { case WorktimeBaseDay: return u.ArbeitszeitProTagFrac(float32(p.worktime) / 100) diff --git a/Backend/models/user.go b/Backend/models/user.go index 5389c33..cf74fd9 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -33,7 +33,7 @@ type User struct { func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) { var user User var err error - if helper.GetEnv("GO_ENV", "production") == "debug" { + if helper.IsDebug() { user, err = GetUserByPersonalNr(123) } else { if !Session.Exists(ctx, "user") { @@ -50,15 +50,15 @@ func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, } // Returns the actual overtime for this moment -func (u *User) GetReportedOvertime() (time.Duration, error) { +func (u *User) GetReportedOvertime(startDate time.Time) (time.Duration, error) { var overtime time.Duration - qStr, err := DB.Prepare("SELECT COALESCE(SUM(EXTRACT(EPOCH FROM ueberstunden) * 1000000000)::BIGINT, 0) AS total_ueberstunden_ns FROM wochen_report WHERE personal_nummer = $1;") + qStr, err := DB.Prepare("SELECT COALESCE(SUM(EXTRACT(EPOCH FROM ueberstunden) * 1000000000)::BIGINT, 0) AS total_ueberstunden_ns FROM wochen_report WHERE personal_nummer = $1 AND woche_start::DATE <= $2::DATE;") if err != nil { return 0, err } defer qStr.Close() - err = qStr.QueryRow(u.PersonalNummer).Scan(&overtime) + err = qStr.QueryRow(u.PersonalNummer, startDate).Scan(&overtime) if err != nil { return 0, err } diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index 3e9eb05..299bf8f 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -151,12 +151,21 @@ func (d *WorkDay) Type() DayType { return DayTypeWorkday } -func (d *WorkDay) GenerateKurzArbeitBookings(u User) (time.Time, time.Time) { +func (d *WorkDay) GenerateKurzArbeitBookings(u User, weekBase WorktimeBase) (time.Time, time.Time) { var timeFrom, timeTo time.Time if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() { return timeFrom, timeTo } + if d.IsEmpty() { + switch weekBase { + case WorktimeBaseDay: + return d.Day.Add(time.Hour * 8), d.Day.Add(time.Hour * 8).Add(u.ArbeitszeitProTag()) + case WorktimeBaseWeek: + return d.Day.Add(time.Hour * 8), d.Day.Add(time.Hour * 8).Add(u.ArbeitszeitProWocheFrac(0.2)) + } + } + timeFrom = d.Bookings[len(d.Bookings)-1].Timestamp.Add(time.Minute) timeTo = timeFrom.Add(u.ArbeitszeitProTag() - d.GetWorktime(u, WorktimeBaseDay, false)) slog.Debug("Added duration as Kurzarbeit", "date", d.Date().String(), "duration", timeTo.Sub(timeFrom).String()) @@ -169,7 +178,7 @@ func (d *WorkDay) GetKurzArbeit() *Absence { } func (d *WorkDay) ToString() string { - return fmt.Sprintf("WorkDay: %s with %d bookings and worktime: %s", d.Date().Format(time.DateOnly), len(d.Bookings), helper.FormatDuration(d.workTime)) + return fmt.Sprintf("WorkDay: %s with %d bookings and worktime: %s. Is KurzArbeit %v", d.Date().Format(time.DateOnly), len(d.Bookings), helper.FormatDuration(d.workTime), d.IsKurzArbeit()) } func (d *WorkDay) IsWorkDay() bool { diff --git a/Backend/models/workWeek.go b/Backend/models/workWeek.go index 1dfcbd3..ba32fba 100644 --- a/Backend/models/workWeek.go +++ b/Backend/models/workWeek.go @@ -6,6 +6,7 @@ package models // this type is based on the "wochen_report" table import ( + "arbeitszeitmessung/helper" "database/sql" "errors" "log" @@ -24,10 +25,12 @@ type WorkWeek struct { Days []IWorkDay User User WeekStart time.Time + weekEnd time.Time Worktime time.Duration WorktimeVirtual time.Duration Overtime time.Duration Status WeekStatus + WeekBase WorktimeBase } type WeekStatus int8 @@ -40,10 +43,15 @@ const ( WeekStatusDifferences ) -func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek { +func NewWorkWeekSimple(user User, tsMonday time.Time, populate bool) WorkWeek { + return NewWorkWeek(user, tsMonday, tsMonday.Add(6*24*time.Hour), populate) +} + +func NewWorkWeek(user User, tsStart, tsEnd time.Time, populate bool) WorkWeek { var week WorkWeek = WorkWeek{ User: user, - WeekStart: tsMonday, + WeekStart: tsStart, + weekEnd: tsEnd, Status: WeekStatusNone, } if populate { @@ -53,13 +61,20 @@ func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek { } func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Duration) { - slog.Debug("Populating Workweek for user", "user", w.User) slog.Debug("Got Days with overtime and worktime", slog.String("worktime", worktime.String()), slog.String("overtime", overtime.String())) - w.Days = GetDays(w.User, w.WeekStart, w.WeekStart.Add(6*24*time.Hour), false) + w.Days = GetDays(w.User, w.WeekStart, w.weekEnd, false) + slog.Debug("Populating Workweek for user", "user", w.User.Name, "Days", lenWorkDays(w.Days), "Start", w.WeekStart, "End", w.weekEnd, "workdays", helper.GetWorkingDays(w.WeekStart, w.weekEnd)) + + if lenWorkDays(w.Days) == helper.GetWorkingDays(w.WeekStart, w.weekEnd) { + w.WeekBase = WorktimeBaseWeek + } else { + w.WeekBase = WorktimeBaseDay + } for _, day := range w.Days { - w.Worktime += day.GetWorktime(w.User, WorktimeBaseDay, false) - w.WorktimeVirtual += day.GetWorktime(w.User, WorktimeBaseDay, true) + w.Worktime += day.GetWorktime(w.User, w.WeekBase, false) + w.WorktimeVirtual += day.GetWorktime(w.User, w.WeekBase, true) + slog.Debug("Calculated Worktime", "Day", day.ToString(), "worktime", w.Worktime.String()) } slog.Debug("Got worktime for user", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorktimeVirtual.String()) @@ -79,6 +94,16 @@ func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Durati } } +func lenWorkDays(workDays []IWorkDay) int { + var lenght int + for _, day := range workDays { + if !day.IsEmpty() || day.IsKurzArbeit() { + lenght += 1 + } + } + return lenght +} + func (w *WorkWeek) CheckStatus() WeekStatus { if w.Status != WeekStatusNone { return w.Status diff --git a/Backend/models/workWeek_test.go b/Backend/models/workWeek_test.go index ef989e4..a1360a6 100644 --- a/Backend/models/workWeek_test.go +++ b/Backend/models/workWeek_test.go @@ -20,7 +20,7 @@ func TestNewWorkWeekNoPopulate(t *testing.T) { if err != nil { t.Fatal(err) } - workWeek := models.NewWorkWeek(testUser, monday, false) + workWeek := models.NewWorkWeekSimple(testUser, monday, false) if workWeek.User != testUser || workWeek.WeekStart != monday { t.Error("No populate workweek does not have right values!") diff --git a/Backend/static/css/styles.css b/Backend/static/css/styles.css index b26c407..004d9fa 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -202,15 +202,9 @@ .top-0 { top: calc(var(--spacing) * 0); } - .top-1 { - top: calc(var(--spacing) * 1); - } .top-1\/2 { top: calc(1/2 * 100%); } - .top-2 { - top: calc(var(--spacing) * 2); - } .top-2\.5 { top: calc(var(--spacing) * 2.5); } @@ -220,15 +214,9 @@ .right-1 { right: calc(var(--spacing) * 1); } - .right-2 { - right: calc(var(--spacing) * 2); - } .right-2\.5 { right: calc(var(--spacing) * 2.5); } - .left-1 { - left: calc(var(--spacing) * 1); - } .left-1\/2 { left: calc(1/2 * 100%); } @@ -253,9 +241,6 @@ .my-2 { margin-block: calc(var(--spacing) * 2); } - .my-4 { - margin-block: calc(var(--spacing) * 4); - } .mt-1 { margin-top: calc(var(--spacing) * 1); } @@ -428,20 +413,9 @@ width: calc(var(--spacing) * 6); height: calc(var(--spacing) * 6); } - .size-8 { - width: calc(var(--spacing) * 8); - height: calc(var(--spacing) * 8); - } - .size-10 { - width: calc(var(--spacing) * 10); - height: calc(var(--spacing) * 10); - } .h-2 { height: calc(var(--spacing) * 2); } - .h-3 { - height: calc(var(--spacing) * 3); - } .h-3\.5 { height: calc(var(--spacing) * 3.5); } @@ -466,9 +440,6 @@ .w-2 { width: calc(var(--spacing) * 2); } - .w-3 { - width: calc(var(--spacing) * 3); - } .w-3\.5 { width: calc(var(--spacing) * 3.5); } @@ -478,9 +449,6 @@ .w-5 { width: calc(var(--spacing) * 5); } - .w-9 { - width: calc(var(--spacing) * 9); - } .w-9\/10 { width: calc(9/10 * 100%); } @@ -493,9 +461,6 @@ .w-full { width: 100%; } - .flex-shrink { - flex-shrink: 1; - } .flex-shrink-0 { flex-shrink: 0; } @@ -511,21 +476,10 @@ .basis-\[content\] { flex-basis: content; } - .border-collapse { - border-collapse: collapse; - } - .-translate-x-1 { - --tw-translate-x: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .-translate-x-1\/2 { --tw-translate-x: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); } - .-translate-y-1 { - --tw-translate-y: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -536,9 +490,6 @@ .cursor-pointer { cursor: pointer; } - .resize { - resize: both; - } .scroll-m-2 { scroll-margin: calc(var(--spacing) * 2); } @@ -665,18 +616,12 @@ .bg-red-600 { background-color: var(--color-red-600); } - .mask-repeat { - mask-repeat: repeat; - } .p-1 { padding: calc(var(--spacing) * 1); } .p-2 { padding: calc(var(--spacing) * 2); } - .px-2 { - padding-inline: calc(var(--spacing) * 2); - } .px-3 { padding-inline: calc(var(--spacing) * 3); } @@ -737,16 +682,9 @@ .uppercase { text-transform: uppercase; } - .underline { - text-decoration-line: underline; - } .opacity-0 { opacity: 0%; } - .outline { - outline-style: var(--tw-outline-style); - outline-width: 1px; - } .filter { filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } @@ -1180,11 +1118,6 @@ syntax: "*"; inherits: false; } -@property --tw-outline-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} @property --tw-blur { syntax: "*"; inherits: false; @@ -1257,7 +1190,6 @@ --tw-border-style: solid; --tw-divide-y-reverse: 0; --tw-font-weight: initial; - --tw-outline-style: solid; --tw-blur: initial; --tw-brightness: initial; --tw-contrast: initial; diff --git a/Backend/templates/reportPage.templ b/Backend/templates/reportPage.templ index 8942fc0..7e2ceb9 100644 --- a/Backend/templates/reportPage.templ +++ b/Backend/templates/reportPage.templ @@ -66,7 +66,7 @@ templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
@timeGaugeComponent(int8(progress), false)
-

Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDuration(week.Worktime)) }

+

Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Worktime, true)) }

Überstunden: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true)) }