Files
arbeitszeitmessung/Backend/models/workDay.go
Tom Tröger 76b23133d0
Some checks failed
Tests / Run Go Tests (push) Failing after 1m20s
fixed #61, #62 refactored getTime variants
2025-12-12 12:26:40 +01:00

280 lines
8.2 KiB
Go

package models
import (
"arbeitszeitmessung/helper"
"encoding/json"
"fmt"
"log"
"log/slog"
"sort"
"time"
)
type WorkDay struct {
Day time.Time `json:"day"`
Bookings []Booking `json:"bookings"`
workTime time.Duration
pauseTime time.Duration
TimeFrom time.Time
TimeTo time.Time
kurzArbeit bool
kurzArbeitAbsence Absence
// Urlaub untertags
worktimeAbsece Absence
}
type WorktimeBase string
const (
WorktimeBaseWeek WorktimeBase = "week"
WorktimeBaseDay WorktimeBase = "day"
)
func (d *WorkDay) GetWorktimeAbsence() Absence {
return d.worktimeAbsece
}
// Gets the time as is in the db (with corrected pause times)
func (d *WorkDay) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
if includeKurzarbeit && d.IsKurzArbeit() && len(d.Bookings) > 0 {
return d.kurzArbeitAbsence.GetWorktime(u, base, true)
}
work, pause := calcWorkPause(d.Bookings)
work, pause = correctWorkPause(work, pause)
if (d.worktimeAbsece != Absence{}) {
work += d.worktimeAbsece.GetWorktime(u, base, false)
}
return work.Round(time.Minute)
}
// Gets the corrected pause times based on db entries
func (d *WorkDay) GetPausetime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
work, pause := calcWorkPause(d.Bookings)
work, pause = correctWorkPause(work, pause)
return pause.Round(time.Minute)
}
// Returns the overtime based on the db entries
func (d *WorkDay) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
work := d.GetWorktime(u, base, includeKurzarbeit)
var targetHours time.Duration
switch base {
case WorktimeBaseDay:
targetHours = u.ArbeitszeitProTag()
case WorktimeBaseWeek:
targetHours = u.ArbeitszeitProWocheFrac(0.2)
}
return (work - targetHours).Round(time.Minute)
}
func (d *WorkDay) GetTimes(u User, base WorktimeBase, includeKurzarbeit bool) (work, pause, overtime time.Duration) {
return d.GetWorktime(u, base, includeKurzarbeit), d.GetPausetime(u, base, includeKurzarbeit), d.GetOvertime(u, base, includeKurzarbeit)
}
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.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() {
return timeFrom, timeTo
}
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())
return timeFrom, timeTo
}
func (d *WorkDay) GetKurzArbeit() *Absence {
return &d.kurzArbeitAbsence
}
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{}
}
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
}
// 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.GetWorktime(u, WorktimeBaseDay, true)
progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100
return int8(progress)
}