Files
arbeitszeitmessung/Backend/models/booking.go
Tom Tröger 8bb1777519 feat: booking can only in between specified hours
every booking happening outside these hours will be clamped to the hours
also added few more config options + regex filters
2026-02-25 01:02:15 +01:00

380 lines
10 KiB
Go

package models
// this file has all functions and types to handle bookings
// the bookings itself are later combined to a workday to save on
// db requests and computation
//
// the booking data is based on the entries in the "anwesenheit" database table
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/logs"
"database/sql"
"fmt"
"log"
"log/slog"
"net/url"
"strconv"
"time"
)
type SameBookingError struct{}
type BookingType struct {
Id int8 `json:"anwesenheit_id"`
Name string `json:"anwesenheit_name"`
}
func (e SameBookingError) Error() string {
return "the same booking already exists!"
}
type Booking struct {
CardUID string `json:"card_uid"`
GeraetID int16 `json:"geraet_id"`
CheckInOut int16 `json:"check_in_out"`
Timestamp time.Time `json:"timestamp"`
CounterId int `json:"counter_id"`
BookingType BookingType `json:"anwesenheit_typ"`
Valid bool `json:"valid"`
}
type BookingOptions struct {
AllowOutOfBounds bool
AllowUnknownUser bool
}
type IDatabase interface {
Prepare(query string) (*sql.Stmt, error)
Exec(query string, args ...any) (sql.Result, error)
}
var DB IDatabase
var Options BookingOptions
func (b *Booking) NewBooking(cardUid string, gereatId int16, checkInOut int16, typeId int8) Booking {
bookingType, err := GetBookingTypeById(typeId)
if err != nil {
log.Printf("Cannot get booking type %d, from database!", typeId)
}
return Booking{
CardUID: cardUid,
GeraetID: gereatId,
CheckInOut: checkInOut,
BookingType: bookingType,
}
}
func (b *Booking) FromUrlParams(params url.Values) Booking {
var booking Booking
if _check_in_out, err := strconv.Atoi(params.Get("check_in_out")); err == nil {
booking.CheckInOut = int16(_check_in_out)
}
if _geraet_id, err := strconv.Atoi(params.Get("geraet_id")); err == nil {
booking.GeraetID = int16(_geraet_id)
}
if _booking_type, err := strconv.Atoi(params.Get("booking_type")); err == nil {
booking.BookingType.Id = int8(_booking_type)
}
booking.CardUID = params.Get("card_uid")
return booking
}
func (b *Booking) Verify() bool {
//check for overlapping time + arbeitszeit verstoß
if b.CardUID == "" { //|| b.GeraetID == 0 || b.CheckInOut == 0 {
log.Println("Booking verify failed invalid CardUID!")
return false
}
if b.CheckInOut == 0 {
log.Println("Booking verify failed invalid CheckInOut!")
return false
}
if bookingType, err := GetBookingTypeById(b.BookingType.Id); err != nil {
log.Println("Booking verify failed invalid BookingType.Id!")
return false
} else {
b.BookingType.Name = bookingType.Name
}
user, err := GetUserByCardUID(b.CardUID)
if err == sql.ErrNoRows {
log.Println("Cannot find user with given CardUID")
return Options.AllowUnknownUser // if allow do not fail verify if not allow fail verify
}
if err != nil {
slog.Error("Cannot get user from CardUID", "error", err)
return false
}
if bookingOutOfBounds(b, &user) {
auditLog, closeLog := logs.NewAudit()
defer closeLog()
if !Options.AllowOutOfBounds {
return false
}
oldTime := b.Timestamp
if oldTime.IsZero() {
oldTime = time.Now()
}
if b.CheckInOut%2 == 1 && b.CheckInOut < 200 { //kommen Booking
b.Timestamp = user.ArbeitMinStartTime(oldTime)
} else {
b.Timestamp = user.ArbeitMaxEndeTime(oldTime)
}
auditLog.Printf("Buchung (%s) von '%s' außerhalb der regulaeren Zeit. Verschieben der Zeit %s -> %s", b.GetBookingType(), user.CardUID, oldTime.Format(time.TimeOnly), b.Timestamp.Format(time.TimeOnly))
slog.Info("Booking is out of work time bounds, setting time to match worktime bounds", "new_time", b.Timestamp.String(), "old_time", oldTime)
}
return true
}
func (b *Booking) Insert() error {
if !b.Timestamp.IsZero() {
return b.InsertWithTimestamp()
}
if !checkLastBooking(*b) {
return SameBookingError{}
}
stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, anwesenheit_typ) VALUES ($1, $2, $3, $4) RETURNING counter_id, timestamp`))
if err != nil {
return err
}
err = stmt.QueryRow(b.CardUID, b.GeraetID, b.CheckInOut, b.BookingType.Id).Scan(&b.CounterId, &b.Timestamp)
if err != nil {
return err
}
return nil
}
func (b *Booking) InsertWithTimestamp() error {
if b.Timestamp.IsZero() {
return b.Insert()
}
if !checkLastBooking(*b) {
return SameBookingError{}
}
stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, anwesenheit_typ, timestamp) VALUES ($1, $2, $3, $4, $5) RETURNING counter_id`))
if err != nil {
return err
}
err = stmt.QueryRow(b.CardUID, b.GeraetID, b.CheckInOut, b.BookingType.Id, b.Timestamp).Scan(&b.CounterId)
if err != nil {
return err
}
return nil
}
func (b *Booking) GetBookingById(booking_id int) (Booking, error) {
var booking Booking
qStr, err := DB.Prepare((`SELECT counter_id, timestamp, card_uid, geraet_id, check_in_out, anwesenheit_typ FROM anwesenheit WHERE counter_id = $1`))
if err != nil {
return booking, err
}
// TODO: also get booking type name
err = qStr.QueryRow(booking_id).Scan(&booking.CounterId, &booking.Timestamp, &booking.CardUID, &booking.GeraetID, &booking.CheckInOut, &booking.BookingType.Id)
if err != nil {
return booking, err
}
return booking, nil
}
// Gets all booking based on a card uid
//
// optional filter parameter
func (b *Booking) GetBookingsByCardID(card_uid string, tsFrom time.Time, tsTo time.Time) ([]Booking, error) {
qStr, err := DB.Prepare((`SELECT counter_id, timestamp, card_uid, geraet_id, check_in_out FROM anwesenheit WHERE card_uid = $1 AND timestamp BETWEEN $2 AND $3 ORDER BY timestamp`))
if err != nil {
return nil, err
}
defer qStr.Close()
var bookings []Booking
rows, err := qStr.Query(card_uid, tsFrom, tsTo)
if err == sql.ErrNoRows {
return bookings, err
}
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var booking Booking
if err := rows.Scan(&booking.CounterId, &booking.Timestamp, &booking.CardUID, &booking.GeraetID, &booking.CheckInOut); err != nil {
return bookings, err
}
bookings = append(bookings, booking)
}
if err = rows.Err(); err != nil {
return bookings, err
}
return bookings, nil
}
func (b Booking) Save() {
qStr, err := DB.Prepare((`UPDATE "anwesenheit" SET "card_uid" = $2, "geraet_id" = $3, "check_in_out" = $4, "timestamp" = $5 WHERE "counter_id" = $1;`))
if err != nil {
log.Fatalf("Error preparing query: %v", err)
return
}
_, err = qStr.Query(b.CounterId, b.CardUID, b.GeraetID, b.CheckInOut, b.Timestamp)
if err != nil {
log.Fatalf("Error executing query: %v", err)
return
}
}
func (b *Booking) GetBookingType() string {
debug := helper.IsDebug()
switch b.CheckInOut {
case 1: //manuelle Änderung
return "kommen"
case 3:
if debug {
return "kommen manuell"
}
return "kommen"
case 2: //manuelle Änderung
return "gehen"
case 4:
if debug {
return "gehen manuell"
}
return "gehen"
case 254:
return "abgemeldet"
default:
return "Buchungs Typ unbekannt"
}
}
func (b *Booking) Update(nb Booking) {
auditLog, closeLog := logs.NewAudit()
defer closeLog()
if b.CheckInOut != nb.CheckInOut && nb.CheckInOut != 0 {
b.CheckInOut = nb.CheckInOut
}
if b.CardUID != nb.CardUID && nb.CardUID != "" {
b.CardUID = nb.CardUID
}
if b.GeraetID != nb.GeraetID && nb.GeraetID != 0 {
b.GeraetID = nb.GeraetID
}
if b.Timestamp != nb.Timestamp {
auditLog.Printf("Änderung in Buchung %d von '%s': Buchungszeit (%s -> %s).", b.CounterId, b.CardUID, b.Timestamp.Format(time.TimeOnly), nb.Timestamp.Format(time.TimeOnly))
b.Timestamp = nb.Timestamp
}
}
func checkLastBooking(b Booking) bool {
var check_in_out int
var timestamp time.Time
slog.Debug("Checking with timestamp:", "timestamp", b.Timestamp)
stmt, err := DB.Prepare((`SELECT check_in_out, timestamp FROM "anwesenheit" WHERE "card_uid" = $1 AND "timestamp" <= $2 ORDER BY "timestamp" DESC LIMIT 1;`))
if err != nil {
log.Fatalf("Error preparing query: %v", err)
return false
}
err = stmt.QueryRow(b.CardUID, b.Timestamp).Scan(&check_in_out, &timestamp)
slog.Info("Checking last bookings check_in_out", "Check", check_in_out)
if err == sql.ErrNoRows {
return true
}
if err != nil {
log.Println("Error checking last booking: ", err)
return false
}
if int16(check_in_out)%2 == b.CheckInOut%2 {
return false
}
if timestamp.Equal(b.Timestamp) {
return false
}
return true
}
func (b *Booking) UpdateTime(newTime time.Time) {
hour, minute, _ := newTime.Clock()
if hour == b.Timestamp.Hour() && minute == b.Timestamp.Minute() {
return
}
var newBooking Booking
newBooking.Timestamp = time.Date(b.Timestamp.Year(), b.Timestamp.Month(), b.Timestamp.Day(), hour, minute, 0, 0, b.Timestamp.Location())
if b.CheckInOut < 3 {
newBooking.CheckInOut = b.CheckInOut + 2
}
if b.CheckInOut == 254 {
newBooking.CheckInOut = 4
}
b.Update(newBooking)
if b.Verify() {
b.Save()
} else {
log.Println("Cannot save updated booking!", b.ToString())
}
}
func (b *Booking) ToString() string {
return fmt.Sprintf("Booking %d: at: %s, CheckInOut: %d, TypeId: %d", b.CounterId, b.Timestamp.Format("15:04"), b.CheckInOut, b.BookingType.Id)
}
func GetBookingTypes() ([]BookingType, error) {
var types []BookingType
qStr, err := DB.Prepare("SELECT anwesenheit_id, anwesenheit_name FROM s_anwesenheit_typen;")
if err != nil {
return types, err
}
defer qStr.Close()
rows, err := qStr.Query()
if err != nil {
log.Println("Error getting anwesenheit rows!", err)
return types, err
}
defer rows.Close()
for rows.Next() {
var bookingType BookingType
if err := rows.Scan(&bookingType.Id, &bookingType.Name); err != nil {
log.Println("Error scanning row!", err)
}
types = append(types, bookingType)
}
return types, nil
}
func GetBookingTypeById(bookingTypeId int8) (BookingType, error) {
var bookingType BookingType = BookingType{Id: bookingTypeId}
qStr, err := DB.Prepare("SELECT anwesenheit_name FROM s_anwesenheit_typen WHERE anwesenheit_id = $1;")
if err != nil {
return bookingType, err
}
defer qStr.Close()
err = qStr.QueryRow(bookingTypeId).Scan(&bookingType.Name)
if err != nil {
return bookingType, err
}
return bookingType, nil
}
func GetBookingTypesCached() []BookingType {
types, err := definedTypes.Get("s_anwesenheit_typen")
if err != nil {
return []BookingType{}
}
return types.([]BookingType)
}
func bookingOutOfBounds(b *Booking, u *User) bool {
bookingTime := b.Timestamp
if b.Timestamp.IsZero() {
bookingTime = time.Now()
}
res := bookingTime.Before(u.ArbeitMinStartTime(bookingTime)) || bookingTime.After(u.ArbeitMaxEndeTime(bookingTime))
return res
}