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 } 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 } func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay { var allDays map[string]IWorkDay = make(map[string]IWorkDay) var sortedDays []IWorkDay for _, day := range GetWorkDays(user, tsFrom, tsTo) { allDays[day.Date().Format("2006-01-02")] = &day } absences, err := GetAbsencesByCardUID(user.CardUID, tsFrom, tsTo) if err != nil { log.Println("Error gettings absences for all Days!", err) return sortedDays } for _, day := range absences { if helper.IsWeekend(day.Date()) { continue } if day.AbwesenheitTyp.WorkTime == 1 { if workDay, ok := allDays[day.Date().Format("2006-01-02")].(*WorkDay); ok { if len(workDay.Bookings) > 0 { workDay.kurzArbeit = true workDay.kurzArbeitAbsence = day } } } else { allDays[day.Date().Format("2006-01-02")] = &day } } for _, day := range allDays { sortedDays = append(sortedDays, day) } if orderedForward { 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) 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("2006-01-02"), 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 // }