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"
This commit is contained in:
2026-02-12 16:46:57 +01:00
parent 8911165c4b
commit 46218f9bca
18 changed files with 365 additions and 178 deletions

View File

@@ -21,66 +21,7 @@ import (
const DE_DATE string = "02.01.2006" const DE_DATE string = "02.01.2006"
const FILE_YEAR_MONTH string = "2006_01" const FILE_YEAR_MONTH string = "2006_01"
func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) { const PDF_DIRECTORY = "/home/tom/Code/arbeitszeitmessung/Backend/doc"
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
}
func PDFCreateController(w http.ResponseWriter, r *http.Request) { func PDFCreateController(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r) helper.RequiresLogin(Session, w, r)
@@ -101,14 +42,16 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
return return
} }
n := 0 if !helper.IsDebug() {
for _, e := range employes { n := 0
if user.IsSuperior(e) { for _, e := range employes {
employes[n] = e if user.IsSuperior(e) {
n++ employes[n] = e
n++
}
} }
employes = employes[:n]
} }
employes = employes[:n]
reportData := createReports(employes, startDate) 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)) slog.Warn("Could not create pdf report", slog.Any("Error", err))
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
w.Header().Set("Content-type", "application/pdf") 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) output.WriteTo(w)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
case "download": case "download":
@@ -131,11 +75,12 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
} }
output, err := zipPfd(pdfReports, &reportData) output, err := zipPfd(pdfReports, &reportData)
if err != nil { 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.WriteHeader(http.StatusInternalServerError)
} }
w.Header().Set("Content-type", "application/zip") 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) output.WriteTo(w)
w.WriteHeader(http.StatusOK) 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 { func createReports(employes []models.User, startDate time.Time) []typstData {
startDate = helper.GetFirstOfMonth(startDate) startDate = helper.GetFirstOfMonth(startDate)
endDate := startDate.AddDate(0, 1, -1) 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) { 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)) targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)-len(publicHolidays))
workDaysThisMonth := models.GetDays(employee, startDate, endDate.AddDate(0, 0, 1), false) daysThisMonth := helper.GenerateDateRange(startDate, endDate)
mondaysThisMonth := helper.GetMondays(daysThisMonth, false)
slog.Debug("Baseline Working hours", "targetHours", targetHoursThisMonth.Hours())
var weeks []models.WorkWeek
var workHours, kurzarbeitHours time.Duration var workHours, kurzarbeitHours time.Duration
for _, day := range workDaysThisMonth { for _, monday := range mondaysThisMonth {
tmpvirtualHours := day.GetWorktime(employee, models.WorktimeBaseDay, true) var week models.WorkWeek
tmpactualHours := day.GetWorktime(employee, models.WorktimeBaseDay, false) if monday.After(startDate) {
if day.IsKurzArbeit() && tmpvirtualHours > tmpactualHours { week = models.NewWorkWeekSimple(employee, monday, true)
slog.Debug("Adding kurzarbeit to workday", "day", day.Date()) } else if startDate.Sub(monday) < time.Hour*24*6 {
kurzarbeitHours += tmpvirtualHours - tmpactualHours 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 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 { if err != nil {
slog.Warn("Failed to convert to days", slog.Any("error", err)) slog.Error("Cannot retrieve total Overtime", "Error", err)
return typstData{}, err
} }
metadata := typstMetadata{ metadata := typstMetadata{
@@ -191,7 +242,7 @@ func createEmployeReport(employee models.User, startDate, endDate time.Time) (ty
Overtime: helper.FormatDurationFill(worktimeBalance, true), Overtime: helper.FormatDurationFill(worktimeBalance, true),
WorkTime: helper.FormatDurationFill(workHours, true), WorkTime: helper.FormatDurationFill(workHours, true),
Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true), Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true),
OvertimeTotal: "", OvertimeTotal: helper.FormatDurationFill(totalOvertime+worktimeBalance, true),
CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), 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 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 var output bytes.Buffer
typstCLI := typst.CLI{ typstCLI := typst.CLI{
WorkingDirectory: "/doc/", WorkingDirectory: PDF_DIRECTORY,
// ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
} }
if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil { 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 var outputMulti []bytes.Buffer
typstRender := typst.CLI{ typstRender := typst.CLI{
WorkingDirectory: "/doc/", WorkingDirectory: PDF_DIRECTORY,
// ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
} }
for _, d := range data { for _, d := range data {
@@ -273,6 +322,16 @@ func zipPfd(pdfReports []bytes.Buffer, reportData *[]typstData) (bytes.Buffer, e
return zipOutput, err 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 { type typstMetadata struct {
TimeRange string `json:"time-range"` TimeRange string `json:"time-range"`
EmployeeName string `json:"employee-name"` EmployeeName string `json:"employee-name"`

View File

@@ -42,7 +42,7 @@ func submitReport(w http.ResponseWriter, r *http.Request) {
return return
} }
workWeek := models.NewWorkWeek(user, weekTs, true) workWeek := models.NewWorkWeekSimple(user, weekTs, true)
switch r.FormValue("method") { switch r.FormValue("method") {
case "send": case "send":
@@ -70,7 +70,7 @@ func showWeeks(w http.ResponseWriter, r *http.Request) {
submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission()) submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission())
lastSub := helper.GetMonday(submissionDate) lastSub := helper.GetMonday(submissionDate)
userWeek := models.NewWorkWeek(user, lastSub, true) userWeek := models.NewWorkWeekSimple(user, lastSub, true)
var workWeeks []models.WorkWeek var workWeeks []models.WorkWeek
teamMembers, err := user.GetTeamMembers() teamMembers, err := user.GetTeamMembers()

View File

@@ -100,7 +100,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
} }
aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, true) 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) user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute)
} else { } else {
log.Println("Cannot calculate overtime: ", err) log.Println("Cannot calculate overtime: ", err)

View File

@@ -20,6 +20,10 @@ func GetEnv(key, fallback string) string {
return fallback return fallback
} }
func IsDebug() bool {
return GetEnv("GO_ENV", "production") == "debug"
}
type CacheItem struct { type CacheItem struct {
value any value any
expiration time.Time expiration time.Time

View File

@@ -4,6 +4,7 @@ package helper
import ( import (
"fmt" "fmt"
"slices"
"time" "time"
) )
@@ -18,6 +19,64 @@ func GetMonday(ts time.Time) time.Time {
return ts 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 { func GetFirstOfMonth(ts time.Time) time.Time {
if ts.Day() > 1 { if ts.Day() > 1 {
return ts.AddDate(0, 0, -(ts.Day() - 1)) return ts.AddDate(0, 0, -(ts.Day() - 1))

View File

@@ -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) { func TestFormatDurationFill(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string

View File

@@ -24,7 +24,7 @@ func SetCors(w http.ResponseWriter) {
func RequiresLogin(session *scs.SessionManager, w http.ResponseWriter, r *http.Request) { func RequiresLogin(session *scs.SessionManager, w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(), "session", session)) r = r.WithContext(context.WithValue(r.Context(), "session", session))
if GetEnv("GO_ENV", "production") == "debug" { if IsDebug() {
return return
} }
if session.Exists(r.Context(), "user") { if session.Exists(r.Context(), "user") {

View File

@@ -38,7 +38,7 @@ func main() {
if err != nil { if err != nil {
slog.Info("No .env file found in directory!") slog.Info("No .env file found in directory!")
} }
if helper.GetEnv("GO_ENV", "production") == "debug" { if helper.IsDebug() {
logLevel.Set(slog.LevelDebug) logLevel.Set(slog.LevelDebug)
envs := os.Environ() envs := os.Environ()
slog.Debug("Debug mode enabled", "Environment Variables", envs) slog.Debug("Debug mode enabled", "Environment Variables", envs)

View File

@@ -63,7 +63,7 @@ func (a *Absence) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool)
return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100) return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100)
case WorktimeBaseWeek: case WorktimeBaseWeek:
if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit { if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit {
return u.ArbeitszeitProTagFrac(0.2) return u.ArbeitszeitProWocheFrac(0.2)
} else if a.AbwesenheitTyp.WorkTime <= 0 { } else if a.AbwesenheitTyp.WorkTime <= 0 {
return 0 return 0
} }

View File

@@ -188,7 +188,7 @@ func (b Booking) Save() {
} }
func (b *Booking) GetBookingType() string { func (b *Booking) GetBookingType() string {
debug := (helper.GetEnv("GO_ENV", "production") == "debug") debug := helper.IsDebug()
switch b.CheckInOut { switch b.CheckInOut {
case 1: //manuelle Änderung case 1: //manuelle Änderung
return "kommen" return "kommen"

View File

@@ -55,7 +55,9 @@ func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay
} }
for _, absentDay := range absences { for _, absentDay := range absences {
if weekDay := absentDay.Date().Weekday(); weekDay == time.Saturday || weekDay == time.Sunday {
continue
}
// Check if there is already a day // Check if there is already a day
existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)] existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)]
switch { switch {

View File

@@ -119,6 +119,8 @@ func (p *PublicHoliday) RequiresAction() bool {
} }
func (p *PublicHoliday) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { func (p *PublicHoliday) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
return 0
switch base { switch base {
case WorktimeBaseDay: case WorktimeBaseDay:
return u.ArbeitszeitProTagFrac(float32(p.worktime) / 100) return u.ArbeitszeitProTagFrac(float32(p.worktime) / 100)

View File

@@ -33,7 +33,7 @@ type User struct {
func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) { func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) {
var user User var user User
var err error var err error
if helper.GetEnv("GO_ENV", "production") == "debug" { if helper.IsDebug() {
user, err = GetUserByPersonalNr(123) user, err = GetUserByPersonalNr(123)
} else { } else {
if !Session.Exists(ctx, "user") { 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 // 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 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 { if err != nil {
return 0, err return 0, err
} }
defer qStr.Close() defer qStr.Close()
err = qStr.QueryRow(u.PersonalNummer).Scan(&overtime) err = qStr.QueryRow(u.PersonalNummer, startDate).Scan(&overtime)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@@ -151,12 +151,21 @@ func (d *WorkDay) Type() DayType {
return DayTypeWorkday 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 var timeFrom, timeTo time.Time
if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() { if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() {
return timeFrom, timeTo 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) timeFrom = d.Bookings[len(d.Bookings)-1].Timestamp.Add(time.Minute)
timeTo = timeFrom.Add(u.ArbeitszeitProTag() - d.GetWorktime(u, WorktimeBaseDay, false)) 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()) 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 { 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 { func (d *WorkDay) IsWorkDay() bool {

View File

@@ -6,6 +6,7 @@ package models
// this type is based on the "wochen_report" table // this type is based on the "wochen_report" table
import ( import (
"arbeitszeitmessung/helper"
"database/sql" "database/sql"
"errors" "errors"
"log" "log"
@@ -24,10 +25,12 @@ type WorkWeek struct {
Days []IWorkDay Days []IWorkDay
User User User User
WeekStart time.Time WeekStart time.Time
weekEnd time.Time
Worktime time.Duration Worktime time.Duration
WorktimeVirtual time.Duration WorktimeVirtual time.Duration
Overtime time.Duration Overtime time.Duration
Status WeekStatus Status WeekStatus
WeekBase WorktimeBase
} }
type WeekStatus int8 type WeekStatus int8
@@ -40,10 +43,15 @@ const (
WeekStatusDifferences 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{ var week WorkWeek = WorkWeek{
User: user, User: user,
WeekStart: tsMonday, WeekStart: tsStart,
weekEnd: tsEnd,
Status: WeekStatusNone, Status: WeekStatusNone,
} }
if populate { 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) { 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())) 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 { for _, day := range w.Days {
w.Worktime += day.GetWorktime(w.User, WorktimeBaseDay, false) w.Worktime += day.GetWorktime(w.User, w.WeekBase, false)
w.WorktimeVirtual += day.GetWorktime(w.User, WorktimeBaseDay, true) 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()) 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 { func (w *WorkWeek) CheckStatus() WeekStatus {
if w.Status != WeekStatusNone { if w.Status != WeekStatusNone {
return w.Status return w.Status

View File

@@ -20,7 +20,7 @@ func TestNewWorkWeekNoPopulate(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
workWeek := models.NewWorkWeek(testUser, monday, false) workWeek := models.NewWorkWeekSimple(testUser, monday, false)
if workWeek.User != testUser || workWeek.WeekStart != monday { if workWeek.User != testUser || workWeek.WeekStart != monday {
t.Error("No populate workweek does not have right values!") t.Error("No populate workweek does not have right values!")

View File

@@ -202,15 +202,9 @@
.top-0 { .top-0 {
top: calc(var(--spacing) * 0); top: calc(var(--spacing) * 0);
} }
.top-1 {
top: calc(var(--spacing) * 1);
}
.top-1\/2 { .top-1\/2 {
top: calc(1/2 * 100%); top: calc(1/2 * 100%);
} }
.top-2 {
top: calc(var(--spacing) * 2);
}
.top-2\.5 { .top-2\.5 {
top: calc(var(--spacing) * 2.5); top: calc(var(--spacing) * 2.5);
} }
@@ -220,15 +214,9 @@
.right-1 { .right-1 {
right: calc(var(--spacing) * 1); right: calc(var(--spacing) * 1);
} }
.right-2 {
right: calc(var(--spacing) * 2);
}
.right-2\.5 { .right-2\.5 {
right: calc(var(--spacing) * 2.5); right: calc(var(--spacing) * 2.5);
} }
.left-1 {
left: calc(var(--spacing) * 1);
}
.left-1\/2 { .left-1\/2 {
left: calc(1/2 * 100%); left: calc(1/2 * 100%);
} }
@@ -253,9 +241,6 @@
.my-2 { .my-2 {
margin-block: calc(var(--spacing) * 2); margin-block: calc(var(--spacing) * 2);
} }
.my-4 {
margin-block: calc(var(--spacing) * 4);
}
.mt-1 { .mt-1 {
margin-top: calc(var(--spacing) * 1); margin-top: calc(var(--spacing) * 1);
} }
@@ -428,20 +413,9 @@
width: calc(var(--spacing) * 6); width: calc(var(--spacing) * 6);
height: 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 { .h-2 {
height: calc(var(--spacing) * 2); height: calc(var(--spacing) * 2);
} }
.h-3 {
height: calc(var(--spacing) * 3);
}
.h-3\.5 { .h-3\.5 {
height: calc(var(--spacing) * 3.5); height: calc(var(--spacing) * 3.5);
} }
@@ -466,9 +440,6 @@
.w-2 { .w-2 {
width: calc(var(--spacing) * 2); width: calc(var(--spacing) * 2);
} }
.w-3 {
width: calc(var(--spacing) * 3);
}
.w-3\.5 { .w-3\.5 {
width: calc(var(--spacing) * 3.5); width: calc(var(--spacing) * 3.5);
} }
@@ -478,9 +449,6 @@
.w-5 { .w-5 {
width: calc(var(--spacing) * 5); width: calc(var(--spacing) * 5);
} }
.w-9 {
width: calc(var(--spacing) * 9);
}
.w-9\/10 { .w-9\/10 {
width: calc(9/10 * 100%); width: calc(9/10 * 100%);
} }
@@ -493,9 +461,6 @@
.w-full { .w-full {
width: 100%; width: 100%;
} }
.flex-shrink {
flex-shrink: 1;
}
.flex-shrink-0 { .flex-shrink-0 {
flex-shrink: 0; flex-shrink: 0;
} }
@@ -511,21 +476,10 @@
.basis-\[content\] { .basis-\[content\] {
flex-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 { .-translate-x-1\/2 {
--tw-translate-x: calc(calc(1/2 * 100%) * -1); --tw-translate-x: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y); 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 { .-translate-y-1\/2 {
--tw-translate-y: calc(calc(1/2 * 100%) * -1); --tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -536,9 +490,6 @@
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.resize {
resize: both;
}
.scroll-m-2 { .scroll-m-2 {
scroll-margin: calc(var(--spacing) * 2); scroll-margin: calc(var(--spacing) * 2);
} }
@@ -665,18 +616,12 @@
.bg-red-600 { .bg-red-600 {
background-color: var(--color-red-600); background-color: var(--color-red-600);
} }
.mask-repeat {
mask-repeat: repeat;
}
.p-1 { .p-1 {
padding: calc(var(--spacing) * 1); padding: calc(var(--spacing) * 1);
} }
.p-2 { .p-2 {
padding: calc(var(--spacing) * 2); padding: calc(var(--spacing) * 2);
} }
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
.px-3 { .px-3 {
padding-inline: calc(var(--spacing) * 3); padding-inline: calc(var(--spacing) * 3);
} }
@@ -737,16 +682,9 @@
.uppercase { .uppercase {
text-transform: uppercase; text-transform: uppercase;
} }
.underline {
text-decoration-line: underline;
}
.opacity-0 { .opacity-0 {
opacity: 0%; opacity: 0%;
} }
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.filter { .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,); 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: "*"; syntax: "*";
inherits: false; inherits: false;
} }
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur { @property --tw-blur {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@@ -1257,7 +1190,6 @@
--tw-border-style: solid; --tw-border-style: solid;
--tw-divide-y-reverse: 0; --tw-divide-y-reverse: 0;
--tw-font-weight: initial; --tw-font-weight: initial;
--tw-outline-style: solid;
--tw-blur: initial; --tw-blur: initial;
--tw-brightness: initial; --tw-brightness: initial;
--tw-contrast: initial; --tw-contrast: initial;

View File

@@ -66,7 +66,7 @@ templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
<div class="flex flex-row gap-2 col-span-3"> <div class="flex flex-row gap-2 col-span-3">
@timeGaugeComponent(int8(progress), false) @timeGaugeComponent(int8(progress), false)
<div> <div>
<p>Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDuration(week.Worktime)) }</p> <p>Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Worktime, true)) }</p>
<p>Überstunden: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true)) }</p> <p>Überstunden: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true)) }</p>
</div> </div>
</div> </div>