276 lines
8.0 KiB
Go
276 lines
8.0 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"
|
|
)
|
|
|
|
// 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() {
|
|
return d.kurzArbeitAbsence.GetWorktime(u, base, true)
|
|
}
|
|
work, pause := calcWorkPause(d.Bookings)
|
|
work, pause = correctWorkPause(work, pause)
|
|
if (d.worktimeAbsece != Absence{}) {
|
|
work += d.worktimeAbsece.GetWorktimeReal(u, base)
|
|
}
|
|
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.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) 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)
|
|
}
|