with this change the time calculations for pdf reports should be better line with the reports send as "week_report"
475 lines
13 KiB
Go
475 lines
13 KiB
Go
package models
|
|
|
|
// the workday combines all bookings of a day into a single type and is the third
|
|
// type of workday which implements the IWorkDay interface
|
|
//
|
|
// this is a meta type and not present in the db
|
|
|
|
import (
|
|
"arbeitszeitmessung/helper"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"log/slog"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/lib/pq"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// IsEmpty implements [IWorkDay].
|
|
func (d *WorkDay) IsEmpty() bool {
|
|
return len(d.Bookings) == 0
|
|
}
|
|
|
|
type WorktimeBase int
|
|
|
|
const (
|
|
WorktimeBaseWeek WorktimeBase = 5
|
|
WorktimeBaseDay WorktimeBase = 1
|
|
)
|
|
|
|
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, _ := correctWorkPause(getWorkPause(d))
|
|
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 := getWorkPause(d)
|
|
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 getWorkPause(d *WorkDay) (work, pause time.Duration) {
|
|
//if today calc, else take from db
|
|
if helper.IsSameDate(d.Date(), time.Now()) {
|
|
return calcWorkPause(d.Bookings)
|
|
} else {
|
|
return d.workTime, d.pauseTime
|
|
}
|
|
}
|
|
|
|
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) Type() DayType {
|
|
return DayTypeWorkday
|
|
}
|
|
|
|
func (d *WorkDay) GenerateKurzArbeitBookings(u User, weekBase WorktimeBase) (time.Time, time.Time) {
|
|
var timeFrom, timeTo time.Time
|
|
if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() {
|
|
return timeFrom, timeTo
|
|
}
|
|
|
|
if d.IsEmpty() {
|
|
switch weekBase {
|
|
case WorktimeBaseDay:
|
|
return d.Day.Add(time.Hour * 8), d.Day.Add(time.Hour * 8).Add(u.ArbeitszeitProTag())
|
|
case WorktimeBaseWeek:
|
|
return d.Day.Add(time.Hour * 8), d.Day.Add(time.Hour * 8).Add(u.ArbeitszeitProWocheFrac(0.2))
|
|
}
|
|
}
|
|
|
|
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. Is KurzArbeit %v", d.Date().Format(time.DateOnly), len(d.Bookings), helper.FormatDuration(d.workTime), d.IsKurzArbeit())
|
|
}
|
|
|
|
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
|
|
),
|
|
all_bookings AS (
|
|
SELECT
|
|
a.card_uid,
|
|
a.timestamp,
|
|
a.timestamp::DATE AS work_date,
|
|
a.check_in_out,
|
|
a.counter_id,
|
|
a.anwesenheit_typ,
|
|
sat.anwesenheit_name AS anwesenheit_typ_name,
|
|
LAG(a.check_in_out) OVER (
|
|
PARTITION BY
|
|
a.card_uid,
|
|
a.timestamp::DATE
|
|
ORDER BY
|
|
a.timestamp
|
|
) AS prev_check,
|
|
LAG(a.timestamp) OVER (
|
|
PARTITION BY
|
|
a.card_uid,
|
|
a.timestamp::DATE
|
|
ORDER BY
|
|
a.timestamp
|
|
) AS prev_timestamp
|
|
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::DATE
|
|
AND a.timestamp::DATE <= $3::DATE
|
|
),
|
|
normalized_bookings AS (
|
|
SELECT
|
|
*
|
|
FROM
|
|
all_bookings
|
|
WHERE
|
|
prev_check IS NULL
|
|
OR prev_check <> check_in_out
|
|
)
|
|
SELECT
|
|
d.work_date,
|
|
COALESCE(MIN(b.timestamp), NOW()) AS time_from,
|
|
COALESCE(MAX(b.timestamp), NOW()) AS time_to,
|
|
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
|
|
)
|
|
) AS total_work_seconds,
|
|
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
|
|
)
|
|
) AS total_pause_seconds,
|
|
jsonb_agg(
|
|
jsonb_build_object(
|
|
'check_in_out',
|
|
b.check_in_out,
|
|
'valid',
|
|
coalesce(b.check_in_out != b.prev_check, true),
|
|
'timestamp',
|
|
b.timestamp,
|
|
'counter_id',
|
|
b.counter_id,
|
|
'anwesenheit_typ',
|
|
jsonb_build_object(
|
|
'anwesenheit_id',
|
|
b.anwesenheit_typ,
|
|
'anwesenheit_name',
|
|
b.anwesenheit_typ_name
|
|
)
|
|
)
|
|
ORDER BY
|
|
b.timestamp
|
|
) FILTER (
|
|
WHERE
|
|
b.card_uid IS NOT NULL
|
|
) AS bookings
|
|
FROM
|
|
all_days d
|
|
LEFT JOIN all_bookings b ON b.work_date = d.work_date
|
|
GROUP BY
|
|
d.work_date;
|
|
`)
|
|
|
|
// 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),
|
|
// normalized_bookings AS (
|
|
// SELECT *
|
|
// FROM (
|
|
// SELECT
|
|
// a.card_uid,
|
|
// a.timestamp,
|
|
// a.timestamp::DATE AS work_date,
|
|
// a.check_in_out,
|
|
// a.counter_id,
|
|
// a.anwesenheit_typ,
|
|
// sat.anwesenheit_name AS anwesenheit_typ_name,
|
|
// 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
|
|
// ) t
|
|
// WHERE prev_check IS NULL OR prev_check <> check_in_out
|
|
// ),
|
|
// ordered_bookings AS (
|
|
// SELECT
|
|
// *,
|
|
// LAG(timestamp) OVER (
|
|
// PARTITION BY card_uid, work_date
|
|
// ORDER BY timestamp
|
|
// ) AS prev_timestamp
|
|
// FROM normalized_bookings
|
|
// )
|
|
// 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 {
|
|
slog.Error("Error scanning row!", "Error", err)
|
|
return workDays
|
|
}
|
|
workDay.workTime = time.Duration(workSec * float64(time.Second))
|
|
workDay.pauseTime = time.Duration(pauseSec * float64(time.Second))
|
|
if bookings != nil {
|
|
err = json.Unmarshal(bookings, &workDay.Bookings)
|
|
if err != nil {
|
|
slog.Error("Error parsing bookings JSON!", "Error", err, "Json", bookings)
|
|
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 {
|
|
slog.Error("Error in workday rows!", "Error", 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)
|
|
}
|
|
|
|
func (d *WorkDay) IsSubmittedAndAccepted() bool {
|
|
var isKurzArbeitAccepted bool
|
|
if d.IsKurzArbeit() {
|
|
isKurzArbeitAccepted = d.kurzArbeitAbsence.IsSubmittedAndAccepted()
|
|
}
|
|
|
|
if d.IsEmpty() {
|
|
return isKurzArbeitAccepted
|
|
}
|
|
|
|
qStr, err := DB.Prepare(`SELECT bestaetigt from wochen_report WHERE anwesenheiten @> $1 AND $2 >= woche_start AND $2 < woche_start + INTERVAL '1 week';`) // @> array contains
|
|
if err != nil {
|
|
slog.Warn("Error when preparing SQL Statement", "error", err)
|
|
return false
|
|
}
|
|
|
|
defer qStr.Close()
|
|
var isSubmittedAndChecked bool = false
|
|
|
|
var bookingsIds []int
|
|
for _, booking := range d.Bookings {
|
|
bookingsIds = append(bookingsIds, booking.CounterId)
|
|
}
|
|
|
|
err = qStr.QueryRow(pq.Array(bookingsIds), d.Date()).Scan(&isSubmittedAndChecked)
|
|
if err == sql.ErrNoRows {
|
|
return false
|
|
}
|
|
|
|
if err != nil {
|
|
slog.Warn("Unexpected error when executing SQL Statement", "error", err, "BookingsIds", bookingsIds)
|
|
}
|
|
return isSubmittedAndChecked
|
|
}
|