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 // }