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
This commit is contained in:
2026-02-25 01:02:15 +01:00
parent f21ce9a3c3
commit 8bb1777519
7 changed files with 129 additions and 68 deletions

View File

@@ -39,6 +39,11 @@ type Booking struct {
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)
@@ -46,6 +51,8 @@ type IDatabase interface {
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 {
@@ -92,10 +99,44 @@ func (b *Booking) Verify() bool {
} 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{}
}
@@ -224,20 +265,21 @@ func (b *Booking) Update(nb Booking) {
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("15:04"), nb.Timestamp.Format("15:04)"))
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
slog.Info("Checking with timestamp:", "timestamp", b.Timestamp.String())
stmt, err := DB.Prepare((`SELECT check_in_out FROM "anwesenheit" WHERE "card_uid" = $1 AND "timestamp" <= $2 ORDER BY "timestamp" DESC LIMIT 1;`))
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)
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
@@ -246,9 +288,13 @@ func checkLastBooking(b Booking) bool {
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
}
@@ -257,8 +303,6 @@ func (b *Booking) UpdateTime(newTime time.Time) {
if hour == b.Timestamp.Hour() && minute == b.Timestamp.Minute() {
return
}
// TODO: add check for time overlap
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 {
@@ -268,14 +312,11 @@ func (b *Booking) UpdateTime(newTime time.Time) {
newBooking.CheckInOut = 4
}
b.Update(newBooking)
// TODO Check verify
if b.Verify() {
b.Save()
} else {
log.Println("Cannot save updated booking!", b.ToString())
}
// b.Verify()
// b.Save()
}
func (b *Booking) ToString() string {
@@ -327,3 +368,12 @@ func GetBookingTypesCached() []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
}

View File

@@ -28,6 +28,8 @@ type User struct {
ArbeitszeitPerTag float32 //`json:"arbeitszeit_per_tag"`
ArbeitszeitPerWoche float32 //`json:"arbeitszeit_per_woche"`
Overtime time.Duration
ArbeitMinStart time.Time
ArbeitMaxEnde time.Time
}
func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) {
@@ -65,35 +67,41 @@ func (u *User) GetReportedOvertime(startDate time.Time) (time.Duration, error) {
return overtime, nil
}
func GetAllUsers() ([]User, error) {
qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname,arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten;`))
var users []User
if err != nil {
return users, err
}
defer qStr.Close()
rows, err := qStr.Query()
if err != nil {
return users, err
}
defer rows.Close()
for rows.Next() {
func GetUserByCardUID(cardUid string) (User, error) {
var user User
var user User
if err := rows.Scan(&user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche); err != nil {
log.Println("Error creating user!", err)
continue
}
users = append(users, user)
qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten WHERE card_uid = $1;`))
if err != nil {
return user, err
}
if err = rows.Err(); err != nil {
return users, nil
err = qStr.QueryRow(cardUid).Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde)
if err != nil {
return user, err
}
return users, nil
return user, nil
}
func (u *User) GetAll() ([]User, error) {
qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname FROM s_personal_daten;`))
func (u *User) ArbeitMinStartTime(date time.Time) time.Time {
if date.Hour() > 0 {
date = date.Truncate(24 * time.Hour).Add(-time.Hour)
}
date = date.Truncate(time.Hour)
slog.Info("Date truncate", "date", date)
return date.Add(time.Hour*time.Duration(u.ArbeitMinStart.Hour()) + time.Minute*time.Duration(u.ArbeitMinStart.Minute()))
}
func (u *User) ArbeitMaxEndeTime(date time.Time) time.Time {
if date.Hour() > 0 {
date = date.Truncate(24 * time.Hour).Add(-time.Hour)
}
date = date.Truncate(time.Hour)
slog.Info("Date truncate", "date", date)
return date.Add(time.Hour*time.Duration(u.ArbeitMaxEnde.Hour()) + time.Minute*time.Duration(u.ArbeitMaxEnde.Minute()))
}
func GetAllUsers() ([]User, error) {
qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten;`))
var users []User
if err != nil {
return users, err
@@ -107,7 +115,7 @@ func (u *User) GetAll() ([]User, error) {
for rows.Next() {
var user User
if err := rows.Scan(&user.CardUID, &user.Vorname, &user.Name); err != nil {
if err := rows.Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde); err != nil {
log.Println("Error creating user!", err)
continue
}
@@ -167,11 +175,11 @@ func (u *User) CheckOut() error {
func GetUserByPersonalNr(personalNummer int) (User, error) {
var user User
qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten WHERE personal_nummer = $1;`))
qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten WHERE personal_nummer = $1;`))
if err != nil {
return user, err
}
err = qStr.QueryRow(personalNummer).Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche)
err = qStr.QueryRow(personalNummer).Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde)
if err != nil {
return user, err
@@ -185,7 +193,7 @@ func GetUserByPersonalNrMulti(personalNummerMulti []int) ([]User, error) {
return users, errors.New("No personalNumbers provided")
}
qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten WHERE personal_nummer = ANY($1::int[]);`))
qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten WHERE personal_nummer = ANY($1::int[]);`))
if err != nil {
return users, err
}
@@ -200,7 +208,7 @@ func GetUserByPersonalNrMulti(personalNummerMulti []int) ([]User, error) {
defer rows.Close()
for rows.Next() {
var user User
if err := rows.Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche); err != nil {
if err := rows.Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde); err != nil {
return users, err
}
users = append(users, user)
@@ -246,6 +254,7 @@ func (u *User) ChangePass(password, newPassword string) (bool, error) {
}
func (u *User) GetTeamMembers() ([]User, error) {
var teamMemberPNrs []int
var teamMembers []User
qStr, err := DB.Prepare(`SELECT personal_nummer FROM s_personal_daten WHERE vorgesetzter_pers_nr = $1 ORDER BY "nachname";`)
if err != nil {
@@ -261,12 +270,16 @@ func (u *User) GetTeamMembers() ([]User, error) {
for rows.Next() {
var personalNr int
err := rows.Scan(&personalNr)
user, err := GetUserByPersonalNr(personalNr)
teamMemberPNrs = append(teamMemberPNrs, personalNr)
if err != nil {
log.Println("Error getting user!")
return teamMembers, err
}
teamMembers = append(teamMembers, user)
}
teamMembers, err = GetUserByPersonalNrMulti(teamMemberPNrs)
if err != nil {
log.Println("Error getting users!")
return teamMembers, err
}
return teamMembers, nil
@@ -343,22 +356,6 @@ LIMIT 1;
return lastSub
}
func (u *User) GetFromCardUID(card_uid string) (User, error) {
user := User{}
var err error
qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag FROM s_personal_daten WHERE card_uid = $1;`))
if err != nil {
return user, err
}
err = qStr.QueryRow(card_uid).Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag)
if err != nil {
return user, err
}
return user, nil
}
func (u *User) IsSuperior(e User) bool {
var isSuperior int
qStr, err := DB.Prepare(`SELECT COUNT(1) FROM s_personal_daten WHERE personal_nummer = $1 AND vorgesetzter_pers_nr = $2`)
@@ -372,7 +369,6 @@ func (u *User) IsSuperior(e User) bool {
return false
}
return isSuperior == 1
}
func getMonday(ts time.Time) time.Time {

View File

@@ -424,10 +424,12 @@ GROUP BY
// returns bool wheter the workday was ended with an automatic logout
func (d *WorkDay) RequiresAction() bool {
if len(d.Bookings) == 0 {
return false
for i := range d.Bookings {
if d.Bookings[i].CheckInOut > 250 {
return true
}
}
return d.Bookings[len(d.Bookings)-1].CheckInOut == 254
return false
}
func (d *WorkDay) GetDayProgress(u User) int8 {