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