Files
arbeitszeitmessung/Backend/models/workDay.go
Tom Tröger 7e5eaebca9
Some checks failed
Tests / Run Go Tests (push) Failing after 1m4s
added worktime base to work time calculations
2025-10-31 23:57:42 +01:00

443 lines
13 KiB
Go

package models
import (
"arbeitszeitmessung/helper"
"encoding/json"
"fmt"
"log"
"log/slog"
"sort"
"time"
)
type IWorkDay interface {
Date() time.Time
TimeWorkVirtual(User) time.Duration
TimeWorkReal(User) time.Duration
TimePauseReal(User) (work, pause time.Duration)
TimeOvertimeReal(User) time.Duration
GetAllWorkTimesVirtual(User) (work, pause, overtime time.Duration)
ToString() string
IsWorkDay() bool
IsKurzArbeit() bool
GetDayProgress(User) int8
RequiresAction() bool
GetWorktimeReal(User, WorktimeBase) time.Duration
GetPausetimeReal(User, WorktimeBase) time.Duration
GetOvertimeReal(User, WorktimeBase) time.Duration
GetWorktimeVirtual(User, WorktimeBase) time.Duration
GetPausetimeVirtual(User, WorktimeBase) time.Duration
GetOvertimeVirtual(User, WorktimeBase) time.Duration
GetTimesReal(User, WorktimeBase) (work, pause, overtime time.Duration)
GetTimesVirtual(User, WorktimeBase) (work, pause, overtime time.Duration)
}
type WorkDay struct {
Day time.Time `json:"day"`
Bookings []Booking `json:"bookings"`
workTime time.Duration
pauseTime time.Duration
realWorkTime time.Duration
realPauseTime time.Duration
TimeFrom time.Time
TimeTo time.Time
kurzArbeit bool
kurzArbeitAbsence Absence
}
type WorktimeBase string
const (
WorktimeBaseWeek WorktimeBase = "week"
WorktimeBaseDay WorktimeBase = "day"
)
func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay {
var allDays map[string]IWorkDay = make(map[string]IWorkDay)
for _, day := range GetWorkDays(user, tsFrom, tsTo) {
allDays[day.Date().Format(time.DateOnly)] = &day
}
absences, err := GetAbsencesByCardUID(user.CardUID, tsFrom, tsTo)
if err != nil {
log.Println("Error gettings absences for all Days!", err)
return nil
}
for _, day := range absences {
if helper.IsWeekend(day.Date()) {
continue
}
if day.AbwesenheitTyp.WorkTime == 1 {
if workDay, ok := allDays[day.Date().Format(time.DateOnly)].(*WorkDay); ok && len(workDay.Bookings) > 0 {
workDay.kurzArbeit = true
workDay.kurzArbeitAbsence = day
}
} else {
allDays[day.Date().Format(time.DateOnly)] = &day
}
}
sortedDays := sortDays(allDays, orderedForward)
return sortedDays
}
// Gets the time as is in the db (with corrected pause times)
func (d *WorkDay) GetWorktimeReal(u User, base WorktimeBase) time.Duration {
work, pause := calcWorkPause(d.Bookings)
work, pause = correctWorkPause(work, pause)
return work
}
// Gets the corrected pause times based on db entries
func (d *WorkDay) GetPausetimeReal(u User, base WorktimeBase) time.Duration {
work, pause := calcWorkPause(d.Bookings)
work, pause = correctWorkPause(work, pause)
return pause
}
// Returns the overtime based on the db entries
func (d *WorkDay) GetOvertimeReal(u User, base WorktimeBase) time.Duration {
work, pause := calcWorkPause(d.Bookings)
work, pause = correctWorkPause(work, pause)
var targetHours time.Duration
switch base {
case WorktimeBaseDay:
targetHours = u.ArbeitszeitProTag()
case WorktimeBaseWeek:
targetHours = u.ArbeitszeitProWoche() / 5
}
return work - targetHours
}
// Returns the worktime based on absence or kurzarbeit
func (d *WorkDay) GetWorktimeVirtual(u User, base WorktimeBase) time.Duration {
if !d.IsKurzArbeit() {
return d.GetWorktimeReal(u, base)
}
switch base {
case WorktimeBaseDay:
return u.ArbeitszeitProTag()
case WorktimeBaseWeek:
return u.ArbeitszeitProWoche() / 5
}
return 0
}
func (d *WorkDay) GetPausetimeVirtual(u User, base WorktimeBase) time.Duration {
return d.GetPausetimeReal(u, base)
}
func (d *WorkDay) GetOvertimeVirtual(u User, base WorktimeBase) time.Duration {
work := d.GetWorktimeVirtual(u, base)
var targetHours time.Duration
switch base {
case WorktimeBaseDay:
targetHours = u.ArbeitszeitProTag()
case WorktimeBaseWeek:
targetHours = u.ArbeitszeitProWoche() / 5
}
return work - targetHours
}
func (d *WorkDay) GetTimesReal(u User, base WorktimeBase) (work, pause, overtime time.Duration) {
return d.GetWorktimeReal(u, base), d.GetPausetimeReal(u, base), d.GetOvertimeReal(u, base)
}
func (d *WorkDay) GetTimesVirtual(u User, base WorktimeBase) (work, pause, overtime time.Duration) {
return d.GetWorktimeVirtual(u, base), d.GetPausetimeVirtual(u, base), d.GetOvertimeVirtual(u, base)
}
func calcWorkPause(bookings []Booking) (work, pause time.Duration) {
var lastBooking Booking
for _, b := range bookings {
if b.CheckInOut%2 == 1 {
if !lastBooking.Timestamp.IsZero() {
pause += b.Timestamp.Sub(lastBooking.Timestamp)
}
} else {
work += b.Timestamp.Sub(lastBooking.Timestamp)
}
lastBooking = b
}
if len(bookings)%2 == 1 {
work += time.Since(lastBooking.Timestamp.Local())
}
return work, pause
}
func correctWorkPause(workIn, pauseIn time.Duration) (work, pause time.Duration) {
if workIn <= 6*time.Hour || pauseIn > 45*time.Minute {
return workIn, pauseIn
}
var diff time.Duration
if workIn <= (9*time.Hour) && pauseIn < 30*time.Minute {
diff = 30*time.Minute - pauseIn
} else if pauseIn < 45*time.Minute {
diff = 45*time.Minute - pauseIn
}
work = workIn - diff
pause = pauseIn + diff
return work, pause
}
func sortDays(days map[string]IWorkDay, forward bool) []IWorkDay {
var sortedDays []IWorkDay
for _, day := range days {
sortedDays = append(sortedDays, day)
}
if forward {
sort.Slice(sortedDays, func(i, j int) bool {
return sortedDays[i].Date().After(sortedDays[j].Date())
})
} else {
sort.Slice(sortedDays, func(i, j int) bool {
return sortedDays[i].Date().Before(sortedDays[j].Date())
})
}
return sortedDays
}
func (d *WorkDay) Date() time.Time {
return d.Day
}
func (d *WorkDay) GenerateKurzArbeitBookings(u User) (time.Time, time.Time) {
var timeFrom, timeTo time.Time
if d.workTime >= u.ArbeitszeitProTag() {
return timeFrom, timeTo
}
timeFrom = d.Bookings[len(d.Bookings)-1].Timestamp.Add(time.Minute)
timeTo = timeFrom.Add(u.ArbeitszeitProTag() - d.workTime)
slog.Debug("Added duration as Kurzarbeit", "date", d.Date().String(), "duration", timeTo.Sub(timeFrom).String())
return timeFrom, timeTo
}
func (d *WorkDay) TimeWorkVirtual(u User) time.Duration {
if d.IsKurzArbeit() {
return u.ArbeitszeitProTag()
}
return d.workTime
}
func (d *WorkDay) GetKurzArbeit() *Absence {
return &d.kurzArbeitAbsence
}
func (d *WorkDay) TimeWorkReal(u User) time.Duration {
d.realWorkTime, d.realPauseTime = 0, 0
var lastBooking Booking
for _, booking := range d.Bookings {
if booking.CheckInOut%2 == 1 {
if !lastBooking.Timestamp.IsZero() {
d.realPauseTime += booking.Timestamp.Sub(lastBooking.Timestamp)
}
} else {
d.realWorkTime += booking.Timestamp.Sub(lastBooking.Timestamp)
}
lastBooking = booking
}
if helper.IsSameDate(d.Date(), time.Now()) && len(d.Bookings)%2 == 1 {
d.realWorkTime += time.Since(lastBooking.Timestamp.Local())
}
// slog.Debug("Calculated RealWorkTime for user", "user", u, slog.String("worktime", d.realWorkTime.String()))
return d.realWorkTime
}
func (d *WorkDay) TimeOvertimeReal(u User) time.Duration {
workTime := d.TimeWorkVirtual(u)
if workTime == 0 {
workTime, _ = d.TimePauseReal(u)
}
if helper.IsWeekend(d.Day) && len(d.Bookings) == 0 {
return 0
}
var overtime time.Duration
overtime = workTime - u.ArbeitszeitProTag()
return overtime
}
func (d *WorkDay) TimePauseReal(u User) (work, pause time.Duration) {
if d.realWorkTime == 0 {
d.TimeWorkReal(u)
}
d.workTime, d.pauseTime = d.realWorkTime, d.realPauseTime
if d.realWorkTime <= 6*time.Hour || d.realPauseTime > 45*time.Minute {
return d.realWorkTime, d.realPauseTime
}
if d.realWorkTime <= (9*time.Hour) && d.realPauseTime < 30*time.Minute {
diff := 30*time.Minute - d.pauseTime
d.workTime -= diff
d.pauseTime += diff
} else if d.realPauseTime < 45*time.Minute {
diff := 45*time.Minute - d.pauseTime
d.workTime -= diff
d.pauseTime += diff
}
return d.workTime, d.pauseTime
}
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))
}
func (d *WorkDay) IsWorkDay() bool {
return true
}
func (d *WorkDay) SetKurzArbeit(kurzArbeit bool) {
d.kurzArbeit = kurzArbeit
}
func (d *WorkDay) IsKurzArbeit() bool {
return d.kurzArbeit
}
func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
var workDays []WorkDay
var workSec, pauseSec float64
qStr, err := DB.Prepare(`
WITH all_days AS (
SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date),
ordered_bookings AS (
SELECT
a.timestamp::DATE AS work_date,
a.timestamp,
a.check_in_out,
a.counter_id,
a.anwesenheit_typ,
sat.anwesenheit_name AS anwesenheit_typ_name,
LAG(a.timestamp) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_timestamp,
LAG(a.check_in_out) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_check
FROM anwesenheit a
LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id
WHERE a.card_uid = $1
AND a.timestamp::DATE >= $2
AND a.timestamp::DATE <= $3
)
SELECT
d.work_date,
COALESCE(MIN(b.timestamp), NOW()) AS time_from,
COALESCE(MAX(b.timestamp), NOW()) AS time_to,
COALESCE(
EXTRACT(EPOCH FROM SUM(
CASE
WHEN b.prev_check IN (1, 3) AND b.check_in_out IN (2, 4, 254)
THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0'
END
)), 0
) AS total_work_seconds,
COALESCE(
EXTRACT(EPOCH FROM SUM(
CASE
WHEN b.prev_check IN (2, 4, 254) AND b.check_in_out IN (1, 3)
THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0'
END
)), 0
) AS total_pause_seconds,
COALESCE(jsonb_agg(jsonb_build_object(
'check_in_out', b.check_in_out,
'timestamp', b.timestamp,
'counter_id', b.counter_id,
'anwesenheit_typ', b.anwesenheit_typ,
'anwesenheit_typ', jsonb_build_object(
'anwesenheit_id', b.anwesenheit_typ,
'anwesenheit_name', b.anwesenheit_typ_name
)
) ORDER BY b.timestamp), '[]'::jsonb) AS bookings
FROM all_days d
LEFT JOIN ordered_bookings b ON d.work_date = b.work_date
GROUP BY d.work_date
ORDER BY d.work_date ASC;`)
if err != nil {
log.Println("Error preparing SQL statement", err)
return workDays
}
defer qStr.Close()
rows, err := qStr.Query(user.CardUID, tsFrom, tsTo)
if err != nil {
log.Println("Error getting rows!")
return workDays
}
defer rows.Close()
// emptyDays, _ := strconv.ParseBool(helper.GetEnv("EMPTY_DAYS", "false"))
for rows.Next() {
var workDay WorkDay
var bookings []byte
if err := rows.Scan(&workDay.Day, &workDay.TimeFrom, &workDay.TimeTo, &workSec, &pauseSec, &bookings); err != nil {
log.Println("Error scanning row!", err)
return workDays
}
workDay.workTime = time.Duration(workSec * float64(time.Second))
workDay.pauseTime = time.Duration(pauseSec * float64(time.Second))
err = json.Unmarshal(bookings, &workDay.Bookings)
if err != nil {
log.Println("Error parsing bookings JSON!", err)
return nil
}
// better empty day handling
if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 {
workDay.Bookings = []Booking{}
}
workDay.TimePauseReal(user)
if len(workDay.Bookings) > 1 || !helper.IsWeekend(workDay.Date()) {
workDays = append(workDays, workDay)
}
}
if err = rows.Err(); err != nil {
log.Println("Error in workday rows!", err)
return workDays
}
return workDays
}
func (d *WorkDay) GetAllWorkTimesReal(user User) (work, pause, overtime time.Duration) {
if d.pauseTime == 0 || d.workTime == 0 {
d.TimePauseReal(user)
}
return d.workTime.Round(time.Minute), d.pauseTime.Round(time.Minute), d.TimeOvertimeReal(user)
}
func (d *WorkDay) GetAllWorkTimesVirtual(user User) (work, pause, overtime time.Duration) {
_, pause, overtime = d.GetAllWorkTimesReal(user)
return d.TimeWorkVirtual(user), pause, overtime
}
// returns bool wheter the workday was ended with an automatic logout
func (d *WorkDay) RequiresAction() bool {
if len(d.Bookings) == 0 {
return false
}
return d.Bookings[len(d.Bookings)-1].CheckInOut == 254
}
func (d *WorkDay) GetDayProgress(u User) int8 {
if d.RequiresAction() {
return -1
}
workTime := d.TimeWorkVirtual(u)
progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100
return int8(progress)
}
// func (d *WorkDay) CalcOvertime(user User) time.Duration {
// if d.workTime == 0 {
// d.TimePauseReal(user)
// }
// if helper.IsWeekend(d.Day) && len(d.Bookings) == 0 {
// return 0
// }
// var overtime time.Duration
// overtime = d.workTime - user.ArbeitszeitProTag()
// return overtime
// }