package endpoints import ( "arbeitszeitmessung/helper" "arbeitszeitmessung/helper/paramParser" "arbeitszeitmessung/models" "bytes" "fmt" "log" "log/slog" "net/http" "time" "github.com/Dadido3/go-typst" ) const DE_DATE string = "02.01.2006" 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 if day.IsWorkDay() { 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") typstDayPart.BookingTo = workDay.Bookings[i+1].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, }) } if workdayAbsence := workDay.GetWorktimeAbsence(); (workdayAbsence != models.Absence{}) { typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: workdayAbsence.AbwesenheitTyp.Name}) } } else { absentDay, _ := day.(*models.Absence) typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name}) } return typstDayParts } func PDFCreateController(w http.ResponseWriter, r *http.Request) { helper.RequiresLogin(Session, w, r) switch r.Method { case http.MethodGet: user, err := models.GetUserFromSession(Session, r.Context()) if err != nil { log.Println("Error getting user!") return } pp := paramParser.New(r.URL.Query()) startDate := pp.ParseTimestampFallback("start_date", time.DateOnly, time.Now()) personalNumbers := pp.ParseIntListFallback("employe_list", ",", make([]int, 0)) employes, err := models.GetUserByPersonalNrMulti(personalNumbers) if err != nil { slog.Warn("Error getting employes!", slog.Any("Error", err)) return } output, err := createReports(user, employes, startDate) if err != nil { slog.Warn("Could not create pdf report", slog.Any("Error", err)) } switch pp.ParseStringFallback("output", "render") { case "render": w.Header().Set("Content-type", "application/pdf") output.WriteTo(w) w.WriteHeader(http.StatusOK) case "download": panic("Not implemented") } default: http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) } } func createReports(user models.User, employes []models.User, startDate time.Time) (bytes.Buffer, error) { startDate = helper.GetFirstOfMonth(startDate) endDate := startDate.AddDate(0, 1, -1) var employeData []typstData for _, employe := range employes { if data, err := createEmployeReport(employe, startDate, endDate); err != nil { slog.Warn("Error when creating employeReport", slog.Any("user", employe), slog.Any("error", err)) } else { employeData = append(employeData, data) } } return renderPDF(employeData) } func createEmployeReport(employee models.User, startDate, endDate time.Time) (typstData, error) { targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)) workDaysThisMonth := models.GetDays(employee, startDate, endDate.AddDate(0, 0, 1), false) slog.Debug("Baseline Working hours", "targetHours", targetHoursThisMonth.Hours()) 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 } workHours += tmpvirtualHours } worktimeBalance := workHours - targetHoursThisMonth typstDays, err := convertDaysToTypst(workDaysThisMonth, employee) if err != nil { slog.Warn("Failed to convert to days", slog.Any("error", err)) return typstData{}, err } metadata := typstMetadata{ EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name), TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)), Overtime: helper.FormatDurationFill(worktimeBalance, true), WorkTime: helper.FormatDurationFill(workHours, true), Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true), OvertimeTotal: "", CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), } return typstData{Meta: metadata, Days: typstDays}, nil } func renderPDF(data []typstData) (bytes.Buffer, error) { var markup bytes.Buffer var output bytes.Buffer if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil { return output, err } // Import the template and invoke the template function with the custom data. // Show is used to replace the current document with whatever content the template function in `template.typ` returns. markup.WriteString(` #import "templates/abrechnung.typ": abrechnung #for d in data { abrechnung(d.Meta, d.Days) } `) // Compile the prepared markup with Typst and write the result it into `output.pdf`. typstCLI := typst.DockerExec{ ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"), } if err := typstCLI.Compile(&markup, &output, nil); err != nil { return output, err } return output, nil } type typstMetadata struct { TimeRange string `json:"time-range"` EmployeeName string `json:"employee-name"` WorkTime string `json:"worktime"` Kurzarbeit string `json:"kurzarbeit"` Overtime string `json:"overtime"` OvertimeTotal string `json:"overtime-total"` CurrentTimestamp string `json:"current-timestamp"` } type typstDayPart struct { BookingFrom string `json:"booking-from"` BookingTo string `json:"booking-to"` WorkType string `json:"worktype"` IsWorkDay bool `json:"is-workday"` } type typstDay struct { Date string `json:"date"` DayParts []typstDayPart `json:"day-parts"` Worktime string `json:"worktime"` Pausetime string `json:"pausetime"` Overtime string `json:"overtime"` Kurzarbeit string `json:"kurzarbeit"` IsFriday bool `json:"is-weekend"` } type typstData struct { Meta typstMetadata `json:"meta"` Days []typstDay `json:"days"` }