feat: locking days, if they are submitted and accepted

fixed #76
This commit is contained in:
2026-02-09 00:32:00 +01:00
parent 2d8747c971
commit 8911165c4b
13 changed files with 218 additions and 115 deletions

View File

@@ -13,8 +13,10 @@ package models
// the absence data is based on the entries in the "abwesenheit" database table
import (
"database/sql"
"encoding/json"
"log"
"log/slog"
"time"
)
@@ -295,3 +297,24 @@ func (a *Absence) Delete() error {
_, err = qStr.Exec(a.CounterId)
return err
}
func (a *Absence) IsSubmittedAndAccepted() bool {
qStr, err := DB.Prepare(`SELECT bestaetigt from wochen_report WHERE $1 = ANY(abwesenheiten) 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
err = qStr.QueryRow(a.CounterId, a.Date()).Scan(&isSubmittedAndChecked)
if err == sql.ErrNoRows {
// No rows found ==> not even submitted
return false
}
if err != nil {
slog.Warn("Unexpected error when executing SQL Statement", "error", err)
}
return isSubmittedAndChecked
}

View File

@@ -95,27 +95,6 @@ func (b *Booking) Verify() bool {
return true
}
func (b *Booking) IsSubmittedAndChecked() bool {
qStr, err := DB.Prepare(`SELECT bestaetigt from wochen_report WHERE $1 = ANY(anwesenheiten);`)
if err != nil {
slog.Warn("Error when preparing SQL Statement", "error", err)
return false
}
defer qStr.Close()
var isSubmittedAndChecked bool = false
err = qStr.QueryRow(b.CounterId).Scan(&isSubmittedAndChecked)
if err == sql.ErrNoRows {
// No rows found ==> not even submitted
return false
}
if err != nil {
slog.Warn("Unexpected error when executing SQL Statement", "error", err)
}
return isSubmittedAndChecked
}
func (b *Booking) Insert() error {
if !checkLastBooking(*b) {
return SameBookingError{}

View File

@@ -17,6 +17,17 @@ type CompoundDay struct {
DayParts []IWorkDay
}
// IsSubmittedAndAccepted implements IWorkDay.
func (c *CompoundDay) IsSubmittedAndAccepted() bool {
var isSubmittedAndAccepted = true
for _, day := range c.DayParts {
_isSubmittedAndAccepted := day.IsSubmittedAndAccepted()
isSubmittedAndAccepted = isSubmittedAndAccepted && _isSubmittedAndAccepted
slog.Info("Result from IsSubmittedCheck", "Result", _isSubmittedAndAccepted, "compount", day.ToString())
}
return isSubmittedAndAccepted
}
func NewCompondDay(date time.Time, dayParts ...IWorkDay) *CompoundDay {
return &CompoundDay{Day: date, DayParts: dayParts}
}

View File

@@ -23,6 +23,7 @@ type IWorkDay interface {
GetTimes(User, WorktimeBase, bool) (work, pause, overtime time.Duration)
GetOvertime(User, WorktimeBase, bool) time.Duration
IsEmpty() bool
IsSubmittedAndAccepted() bool
}
type DayType int

View File

@@ -19,6 +19,11 @@ type PublicHoliday struct {
worktime int8
}
// IsSubmittedAndAccepted implements IWorkDay.
func (p *PublicHoliday) IsSubmittedAndAccepted() bool {
return true
}
// IsEmpty implements [IWorkDay].
func (p *PublicHoliday) IsEmpty() bool {
return false

View File

@@ -292,10 +292,42 @@ func (u *User) GetNextWeek() WorkWeek {
func (u *User) GetLastWorkWeekSubmission() time.Time {
var lastSub time.Time
qStr, err := DB.Prepare(`
SELECT COALESCE(
(SELECT woche_start + INTERVAL '1 week' FROM wochen_report WHERE personal_nummer = $1 ORDER BY woche_start DESC LIMIT 1),
(SELECT timestamp FROM anwesenheit WHERE card_uid = $2 ORDER BY timestamp LIMIT 1)
) AS letzte_buchung;
SELECT new_week
FROM (
-- Highest priority
SELECT
woche_start AS new_week,
1 AS priority
FROM wochen_report
WHERE personal_nummer = $1
AND bestaetigt IS NULL
UNION ALL
-- Fallback if #1 returns nothing
SELECT
woche_start + INTERVAL '1 week' AS new_week,
2 AS priority
FROM wochen_report wo
WHERE personal_nummer = $1
AND NOT EXISTS (
SELECT 1
FROM wochen_report wi
WHERE wi.woche_start = wo.woche_start + INTERVAL '1 week'
AND wi.personal_nummer = wo.personal_nummer
)
UNION ALL
-- Final fallback
SELECT
timestamp AS new_week,
3 AS priority
FROM anwesenheit
WHERE card_uid = $2
) t
ORDER BY priority, new_week
LIMIT 1;
`)
if err != nil {
slog.Debug("Error preparing query statement.", "error", err)

View File

@@ -7,12 +7,15 @@ package models
import (
"arbeitszeitmessung/helper"
"database/sql"
"encoding/json"
"fmt"
"log"
"log/slog"
"sort"
"time"
"github.com/lib/pq"
)
type WorkDay struct {
@@ -425,3 +428,38 @@ func (d *WorkDay) GetDayProgress(u User) int8 {
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
}

View File

@@ -34,6 +34,7 @@ type WeekStatus int8
const (
WeekStatusNone WeekStatus = iota
WeekStatusCorrected
WeekStatusSent
WeekStatusAccepted
WeekStatusDifferences
@@ -86,25 +87,31 @@ func (w *WorkWeek) CheckStatus() WeekStatus {
log.Println("Cannot access Database!")
return w.Status
}
qStr, err := DB.Prepare(`SELECT bestaetigt FROM wochen_report WHERE woche_start = $1::DATE AND personal_nummer = $2;`)
qStr, err := DB.Prepare(`SELECT bestaetigt, id FROM wochen_report WHERE woche_start = $1::DATE AND personal_nummer = $2;`)
if err != nil {
log.Println("Error preparing SQL statement", err)
return w.Status
}
defer qStr.Close()
var beastatigt bool
err = qStr.QueryRow(w.WeekStart, w.User.PersonalNummer).Scan(&beastatigt)
var beastatigt sql.NullBool
err = qStr.QueryRow(w.WeekStart, w.User.PersonalNummer).Scan(&beastatigt, &w.Id)
if err == sql.ErrNoRows {
return w.Status
}
slog.Info("Bestätigt query res", "Best", beastatigt, "week", w.Id)
if err != nil {
log.Println("Error querying database", err)
return w.Status
}
if beastatigt {
switch {
case beastatigt.Bool:
w.Status = WeekStatusAccepted
} else {
case beastatigt.Valid:
w.Status = WeekStatusSent
default:
w.Status = WeekStatusCorrected
}
return w.Status
}
@@ -206,23 +213,33 @@ func (w *WorkWeek) SendWeek() error {
return ErrRunningWeek
}
if w.CheckStatus() != WeekStatusNone {
qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE, arbeitszeit = make_interval(secs => $3::numeric / 1000000000), ueberstunden = make_interval(secs => $4::numeric / 1000000000), anwesenheiten=$5, abwesenheiten=$6 WHERE personal_nummer = $1 AND woche_start = $2;`)
if err != nil {
slog.Warn("Error preparing SQL statement", "error", err)
return err
}
} else {
switch w.CheckStatus() {
case WeekStatusNone:
qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden, anwesenheiten, abwesenheiten) VALUES ($1, $2, make_interval(secs => $3::numeric / 1000000000), make_interval(secs => $4::numeric / 1000000000), $5, $6);`)
if err != nil {
slog.Warn("Error preparing SQL statement", "error", err)
return err
}
case WeekStatusCorrected:
qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE, arbeitszeit = make_interval(secs => $3::numeric / 1000000000), ueberstunden = make_interval(secs => $4::numeric / 1000000000), anwesenheiten=$5, abwesenheiten=$6 WHERE personal_nummer = $1 AND woche_start = $2;`)
if err != nil {
slog.Warn("Error preparing SQL statement", "error", err)
return err
}
case WeekStatusSent, WeekStatusAccepted:
qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = null WHERE personal_nummer = $1 AND woche_start = $2 AND ($3::numeric IS NULL OR TRUE) AND ($4::numeric IS NULL OR TRUE) AND ($5::int[] IS NULL OR TRUE) AND ($6::int[] IS NULL OR TRUE);`)
if err != nil {
slog.Warn("Error preparing SQL statement", "error", err)
return err
}
}
_, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, int64(w.Worktime), int64(w.Overtime), pq.Array(anwBookings), pq.Array(awBookings))
if err != nil {
log.Println("Error executing query!", err)
slog.Error("Error executing query!", "error", err)
return err
}
return nil