fixed #63 all scheduled tasks are now under route /auto and will require api key in the future
Some checks failed
Tests / Run Go Tests (push) Failing after 1m32s

This commit is contained in:
2025-12-03 00:20:11 +01:00
parent 6c0a8bca64
commit 386f11ec7e
9 changed files with 138 additions and 34 deletions

View File

@@ -0,0 +1,82 @@
package endpoints
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser"
"arbeitszeitmessung/models"
"encoding/json"
"errors"
"log/slog"
"net/http"
"time"
)
func KurzarbeitFillHandler(w http.ResponseWriter, r *http.Request) {
helper.SetCors(w)
switch r.Method {
case "GET":
fillKurzarbeit(r, w)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func fillKurzarbeit(r *http.Request, w http.ResponseWriter) {
bookingTypeKurzarbeit, err := getKurzarbeitBookingType()
if err != nil {
slog.Info("Error getting BookingType Kurzarbeit %v\n", slog.Any("Error", err))
}
users, err := models.GetAllUsers()
if err != nil {
slog.Info("Error getting user list %v\n", slog.Any("Error", err))
}
pp := paramParser.New(r.URL.Query())
startDate := pp.ParseTimestampFallback("date", time.DateOnly, time.Now())
var kurzarbeitAdded int
for _, user := range users {
days := models.GetDays(user, startDate, startDate.AddDate(0, 0, 1), false)
if len(days) == 0 {
continue
}
day := days[len(days)-1]
if !day.IsKurzArbeit() || !day.IsWorkDay() {
continue
}
if day.GetWorktimeReal(user, models.WorktimeBaseDay) >= day.GetWorktimeVirtual(user, models.WorktimeBaseDay) {
continue
}
worktimeKurzarbeit := day.GetWorktimeVirtual(user, models.WorktimeBaseDay) - day.GetWorktimeReal(user, models.WorktimeBaseDay)
if wDay, ok := day.(*models.WorkDay); !ok || len(wDay.Bookings) == 0 {
continue
}
workday, _ := day.(*models.WorkDay)
lastBookingTime := workday.Bookings[len(workday.Bookings)-1].Timestamp
kurzarbeitBegin := (*models.Booking).New(nil, user.CardUID, 0, 1, bookingTypeKurzarbeit.Id)
kurzarbeitEnd := (*models.Booking).New(nil, user.CardUID, 0, 2, bookingTypeKurzarbeit.Id)
kurzarbeitBegin.Timestamp = lastBookingTime.Add(time.Minute)
kurzarbeitEnd.Timestamp = lastBookingTime.Add(worktimeKurzarbeit)
kurzarbeitBegin.InsertWithTimestamp()
kurzarbeitEnd.InsertWithTimestamp()
kurzarbeitAdded += 1
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(kurzarbeitAdded)
}
func getKurzarbeitBookingType() (models.BookingType, error) {
for _, bookingType := range models.GetBookingTypesCached() {
if bookingType.Name == "Kurzarbeit" {
return bookingType, nil
}
}
return models.BookingType{}, errors.New("No Booking Type found")
}

View File

@@ -20,7 +20,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
} }
func autoLogout(w http.ResponseWriter) { func autoLogout(w http.ResponseWriter) {
users, err := (*models.User).GetAll(nil) users, err := models.GetAllUsers()
var logged_out_users []models.User var logged_out_users []models.User
if err != nil { if err != nil {
fmt.Printf("Error getting user list %v\n", err) fmt.Printf("Error getting user list %v\n", err)

View File

@@ -20,7 +20,7 @@ func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, erro
var typstDays []typstDay var typstDays []typstDay
for _, day := range days { for _, day := range days {
var thisTypstDay typstDay var thisTypstDay typstDay
work, pause, overtime := day.GetTimesVirtual(u, models.WorktimeBaseWeek) work, pause, overtime := day.GetTimesReal(u, models.WorktimeBaseWeek)
thisTypstDay.Date = day.Date().Format(DE_DATE) thisTypstDay.Date = day.Date().Format(DE_DATE)
thisTypstDay.Worktime = helper.FormatDurationFill(work, true) thisTypstDay.Worktime = helper.FormatDurationFill(work, true)
thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true)
@@ -45,15 +45,6 @@ func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDay
typstDayPart.IsWorkDay = true typstDayPart.IsWorkDay = true
typstDayParts = append(typstDayParts, typstDayPart) typstDayParts = append(typstDayParts, typstDayPart)
} }
if day.IsKurzArbeit() {
tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user)
typstDayParts = append(typstDayParts, typstDayPart{
BookingFrom: tsFrom.Format("15:04"),
BookingTo: tsTo.Format("15:04"),
WorkType: "Kurzarbeit",
IsWorkDay: true,
})
}
} else { } else {
absentDay, _ := day.(*models.Absence) absentDay, _ := day.(*models.Absence)
typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name}) typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name})
@@ -61,11 +52,11 @@ func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDay
return typstDayParts return typstDayParts
} }
func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) { func renderPDF(data []typstData) (bytes.Buffer, error) {
var markup bytes.Buffer var markup bytes.Buffer
var output bytes.Buffer var output bytes.Buffer
if err := typst.InjectValues(&markup, map[string]any{"meta": metadata, "days": days}); err != nil { if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil {
return output, err return output, err
} }
@@ -73,18 +64,13 @@ func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) {
// Show is used to replace the current document with whatever content the template function in `template.typ` returns. // Show is used to replace the current document with whatever content the template function in `template.typ` returns.
markup.WriteString(` markup.WriteString(`
#import "templates/abrechnung.typ": abrechnung #import "templates/abrechnung.typ": abrechnung
#show: doc => abrechnung(meta, days) #for d in data {
abrechnung(d.Meta, d.Days)
}
`) `)
// Compile the prepared markup with Typst and write the result it into `output.pdf`. // Compile the prepared markup with Typst and write the result it into `output.pdf`.
// f, err := os.Create("output.pdf")
// if err != nil {
// log.Panicf("Failed to create output file: %v.", err)
// }
// defer f.Close()
//
// typstCLI := typst.CLI{}
typstCLI := typst.DockerExec{ typstCLI := typst.DockerExec{
ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"), ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
} }
@@ -113,7 +99,7 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "application/pdf") w.Header().Set("Content-type", "application/pdf")
output.WriteTo(w) output.WriteTo(w)
w.WriteHeader(200) w.WriteHeader(http.StatusOK)
default: default:
http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed)
@@ -154,7 +140,7 @@ func createEmployeReport(employee models.User, startDate, endDate time.Time) (by
CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"),
} }
return renderPDF(typstDays, metadata) return renderPDF([]typstData{{Meta: metadata, Days: typstDays}, {Meta: metadata, Days: typstDays}})
} }
func PDFHandler(w http.ResponseWriter, r *http.Request) { func PDFHandler(w http.ResponseWriter, r *http.Request) {
@@ -193,7 +179,7 @@ func PDFHandler(w http.ResponseWriter, r *http.Request) {
CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"),
} }
output, err := renderPDF(typstDays, metadata) output, err := renderPDF([]typstData{{Meta: metadata, Days: typstDays}})
if err != nil { if err != nil {
slog.Warn("Could not create pdf report", slog.Any("Error", err)) slog.Warn("Could not create pdf report", slog.Any("Error", err))
} }
@@ -227,3 +213,8 @@ type typstDay struct {
Overtime string `json:"overtime"` Overtime string `json:"overtime"`
IsFriday bool `json:"is-weekend"` IsFriday bool `json:"is-weekend"`
} }
type typstData struct {
Meta typstMetadata `json:"meta"`
Days []typstDay `json:"days"`
}

View File

@@ -59,7 +59,7 @@ func (p *ParamsParser) ParseStringFallback(key string, fallback string) string {
return p.urlParams.Get(key) return p.urlParams.Get(key)
} }
func (p *ParamsParser) ParseString(key string, fallback string) (string, error) { func (p *ParamsParser) ParseString(key string) (string, error) {
if !p.urlParams.Has(key) { if !p.urlParams.Has(key) {
return "", &NoValueError{Key: key} return "", &NoValueError{Key: key}
} }

View File

@@ -30,16 +30,18 @@ func TestFormatDuration(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
duration time.Duration duration time.Duration
fill bool
}{ }{
{"2h", time.Duration(120 * time.Minute)}, {"2h", time.Duration(120 * time.Minute), true},
{"30min", time.Duration(30 * time.Minute)}, {"30min", time.Duration(30 * time.Minute), true},
{"1h 30min", time.Duration(90 * time.Minute)}, {"1h 30min", time.Duration(90 * time.Minute), true},
{"-1h 30min", time.Duration(-90 * time.Minute)}, {"-1h 30min", time.Duration(-90 * time.Minute), true},
{"0min", 0}, {"0min", 0, true},
{"", 0, false},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
if FormatDurationFill(tc.duration, true) != tc.name { if FormatDurationFill(tc.duration, tc.fill) != tc.name {
t.Error("Format missmatch in Formatduration.") t.Error("Format missmatch in Formatduration.")
} }
}) })

View File

@@ -46,7 +46,8 @@ func main() {
server.HandleFunc("/time/new", endpoints.TimeCreateHandler) server.HandleFunc("/time/new", endpoints.TimeCreateHandler)
server.Handle("/absence", ParamsMiddleware(endpoints.AbsencHandler)) server.Handle("/absence", ParamsMiddleware(endpoints.AbsencHandler))
server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler)) server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler))
server.HandleFunc("/logout", endpoints.LogoutHandler) server.HandleFunc("/auto/logout", endpoints.LogoutHandler)
server.HandleFunc("/auto/kurzarbeit", endpoints.KurzarbeitFillHandler)
server.HandleFunc("/user/{action}", endpoints.UserHandler) server.HandleFunc("/user/{action}", endpoints.UserHandler)
// server.HandleFunc("/user/login", endpoints.LoginHandler) // server.HandleFunc("/user/login", endpoints.LoginHandler)
// server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) // server.HandleFunc("/user/settings", endpoints.UserSettingsHandler)

View File

@@ -57,6 +57,34 @@ func (u *User) GetReportedOvertime() (time.Duration, error) {
return overtime, nil 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 {
fmt.Printf("Error preparing query statement %v\n", err)
return users, err
}
defer qStr.Close()
rows, err := qStr.Query()
if err != nil {
return users, err
}
defer rows.Close()
for rows.Next() {
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)
}
if err = rows.Err(); err != nil {
return users, nil
}
return users, nil
}
func (u *User) GetAll() ([]User, error) { func (u *User) GetAll() ([]User, error) {
qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname FROM s_personal_daten;`)) qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname FROM s_personal_daten;`))
var users []User var users []User

View File

@@ -104,7 +104,7 @@ CREATE TABLE "s_abwesenheit_typen" (
"arbeitszeit_equivalent" float4 NOT NULL "arbeitszeit_equivalent" float4 NOT NULL
); );
COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; 1=Arbeitszeit auffüllen; 2=Arbeitszeit austauschen'; COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; -1=Arbeitszeit auffüllen; <=1 => Arbeitszeit';
-- Adds crypto extension -- Adds crypto extension

View File

@@ -11,7 +11,7 @@
columns: (3fr, .65fr), columns: (3fr, .65fr),
align: left + horizon, align: left + horizon,
inset: .5em, inset: .5em,
[#meta.EmployeeName -- #meta.TimeRange], grid.cell(rowspan: 2)[#image("static/logo.png")], [#meta.EmployeeName -- #meta.TimeRange], grid.cell(rowspan: 2)[#image("/static/logo.png")],
[Arbeitszeitrechnung maschinell erstellt am #meta.CurrentTimestamp], [Arbeitszeitrechnung maschinell erstellt am #meta.CurrentTimestamp],
) )
]) ])