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 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"`

View File

@@ -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()

View File

@@ -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)