Compare commits
3 Commits
a634b7a69e
...
ac59d2642f
| Author | SHA1 | Date | |
|---|---|---|---|
| ac59d2642f | |||
| cf5238f024 | |||
| 4bc5594dc5 |
@@ -14,12 +14,14 @@ import (
|
||||
"github.com/Dadido3/go-typst"
|
||||
)
|
||||
|
||||
const DE_DATE string = "02.01.2006"
|
||||
|
||||
func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) {
|
||||
var typstDays []typstDay
|
||||
for _, day := range days {
|
||||
var thisTypstDay typstDay
|
||||
work, pause, overtime := day.GetAllWorkTimesVirtual(u)
|
||||
thisTypstDay.Date = day.Date().Format("02.01.2006")
|
||||
thisTypstDay.Date = day.Date().Format(DE_DATE)
|
||||
thisTypstDay.Worktime = helper.FormatDurationFill(work, true)
|
||||
thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true)
|
||||
thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true)
|
||||
@@ -89,10 +91,71 @@ func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) {
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func PDFCreateController(w http.ResponseWriter, r *http.Request) {
|
||||
helper.RequiresLogin(Session, w, r)
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
user, err := models.GetUserFromSession(Session, r.Context())
|
||||
if err != nil {
|
||||
log.Println("Error getting user!")
|
||||
return
|
||||
}
|
||||
pp := paramParser.New(r.URL.Query())
|
||||
startDate := pp.ParseTimestampFallback("start_date", time.DateOnly, time.Now())
|
||||
var members []models.User = make([]models.User, 0)
|
||||
output, err := createReports(user, members, startDate)
|
||||
if err != nil {
|
||||
slog.Warn("Could not create pdf report", slog.Any("Error", err))
|
||||
}
|
||||
|
||||
w.Header().Set("Content-type", "application/pdf")
|
||||
output.WriteTo(w)
|
||||
w.WriteHeader(200)
|
||||
|
||||
default:
|
||||
http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func createReports(user models.User, employes []models.User, startDate time.Time) (bytes.Buffer, error) {
|
||||
if startDate.Day() > 1 {
|
||||
startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1))
|
||||
}
|
||||
endDate := startDate.AddDate(0, 1, -1)
|
||||
return createEmployeReport(user, startDate, endDate)
|
||||
}
|
||||
|
||||
func createEmployeReport(employee models.User, startDate, endDate time.Time) (bytes.Buffer, error) {
|
||||
targetHours := (employee.ArbeitszeitProWoche() / 5) * time.Duration(helper.GetWorkingDays(startDate, endDate))
|
||||
workingDays := models.GetDays(employee, startDate, endDate, false)
|
||||
|
||||
var actualHours time.Duration
|
||||
for _, day := range workingDays {
|
||||
actualHours += day.TimeWorkVirtual(employee)
|
||||
}
|
||||
worktimeBalance := actualHours - targetHours
|
||||
|
||||
typstDays, err := convertDaysToTypst(workingDays, employee)
|
||||
if err != nil {
|
||||
log.Panicf("Failed to convert days!")
|
||||
}
|
||||
|
||||
metadata := typstMetadata{
|
||||
EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name),
|
||||
TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)),
|
||||
Overtime: helper.FormatDurationFill(worktimeBalance, true),
|
||||
WorkTime: helper.FormatDurationFill(actualHours, true),
|
||||
OvertimeTotal: "",
|
||||
CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"),
|
||||
}
|
||||
|
||||
return renderPDF(typstDays, metadata)
|
||||
}
|
||||
|
||||
func PDFHandler(w http.ResponseWriter, r *http.Request) {
|
||||
helper.RequiresLogin(Session, w, r)
|
||||
pp := paramParser.New(r.URL.Query())
|
||||
startDate := pp.ParseTimestamp("start_date", time.DateOnly, time.Now())
|
||||
startDate := time.Now()
|
||||
|
||||
if startDate.Day() > 1 {
|
||||
startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1))
|
||||
}
|
||||
@@ -118,7 +181,7 @@ func PDFHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
metadata := typstMetadata{
|
||||
EmployeeName: fmt.Sprintf("%s %s", user.Vorname, user.Name),
|
||||
TimeRange: fmt.Sprintf("%s - %s", startDate.Format("02.01.2006"), endDate.Format("02.01.2006")),
|
||||
TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)),
|
||||
Overtime: helper.FormatDurationFill(aggregatedOvertime, true),
|
||||
WorkTime: helper.FormatDurationFill(aggregatedWorkTime, true),
|
||||
OvertimeTotal: "",
|
||||
|
||||
@@ -2,13 +2,13 @@ package endpoints
|
||||
|
||||
import (
|
||||
"arbeitszeitmessung/helper"
|
||||
"arbeitszeitmessung/helper/paramParser"
|
||||
"arbeitszeitmessung/models"
|
||||
"arbeitszeitmessung/templates"
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -30,17 +30,17 @@ func submitReport(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("Error parsing form", err)
|
||||
return
|
||||
}
|
||||
userPN, _ := strconv.Atoi(r.FormValue("user"))
|
||||
_weekTs := r.FormValue("week")
|
||||
weekTs, err := time.Parse(time.DateOnly, _weekTs)
|
||||
pp := paramParser.New(r.Form)
|
||||
userPN, err := pp.ParseInt("user")
|
||||
weekTs := pp.ParseTimestampFallback("week", time.DateOnly, time.Now())
|
||||
user, err := models.GetUserByPersonalNr(userPN)
|
||||
workWeek := models.NewWorkWeek(user, weekTs, true)
|
||||
|
||||
if err != nil {
|
||||
log.Println("Could not get user!")
|
||||
return
|
||||
}
|
||||
|
||||
workWeek := models.NewWorkWeek(user, weekTs, true)
|
||||
|
||||
switch r.FormValue("method") {
|
||||
case "send":
|
||||
err = workWeek.SendWeek()
|
||||
@@ -62,14 +62,11 @@ func showWeeks(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
submissionDate := r.URL.Query().Get("submission_date")
|
||||
lastSub := user.GetLastWorkWeekSubmission()
|
||||
if submissionDate != "" {
|
||||
submissionDate, err := time.Parse(time.DateOnly, submissionDate)
|
||||
if err == nil {
|
||||
lastSub = helper.GetMonday(submissionDate)
|
||||
}
|
||||
}
|
||||
|
||||
pp := paramParser.New(r.URL.Query())
|
||||
submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission())
|
||||
lastSub := helper.GetMonday(submissionDate)
|
||||
|
||||
userWeek := models.NewWorkWeek(user, lastSub, true)
|
||||
|
||||
var workWeeks []models.WorkWeek
|
||||
|
||||
@@ -2,14 +2,15 @@ package endpoints
|
||||
|
||||
import (
|
||||
"arbeitszeitmessung/helper"
|
||||
"arbeitszeitmessung/helper/paramParser"
|
||||
"arbeitszeitmessung/models"
|
||||
"arbeitszeitmessung/templates"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
@@ -67,26 +68,15 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
pp := paramParser.New(r.URL.Query())
|
||||
|
||||
// TODO add config for timeoffset
|
||||
tsFrom, err := parseTimestamp(r, "time_from", time.Now().AddDate(0, -1, 0).Format(time.DateOnly))
|
||||
if err != nil {
|
||||
log.Println("Error parsing 'from' time", err)
|
||||
http.Error(w, "Timestamp 'from' cannot be parsed!", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tsTo, err := parseTimestamp(r, "time_to", time.Now().Format(time.DateOnly))
|
||||
if err != nil {
|
||||
log.Println("Error parsing 'to' time", err)
|
||||
http.Error(w, "Timestamp 'to' cannot be parsed!", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tsFrom := pp.ParseTimestampFallback("time_from", time.DateOnly, time.Now().AddDate(0, -1, 0))
|
||||
tsTo := pp.ParseTimestampFallback("time_to", time.DateOnly, time.Now())
|
||||
|
||||
tsTo = tsTo.AddDate(0, 0, 1) // so that today is inside
|
||||
|
||||
days := models.GetDays(user, tsFrom, tsTo, true)
|
||||
sort.Slice(days, func(i, j int) bool {
|
||||
return days[i].Date().After(days[j].Date())
|
||||
})
|
||||
|
||||
lastSub := user.GetLastWorkWeekSubmission()
|
||||
var aggregatedOvertime time.Duration
|
||||
@@ -116,6 +106,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func updateBooking(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
pp := paramParser.New(r.Form)
|
||||
var loc *time.Location
|
||||
loc, err := time.LoadLocation(helper.GetEnv("TZ", "Europe/Berlin"))
|
||||
if err != nil {
|
||||
@@ -136,10 +127,9 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var check_in_out int
|
||||
check_in_out, err = strconv.Atoi(r.FormValue("check_in_out"))
|
||||
check_in_out, err := pp.ParseInt("check_in_out")
|
||||
if err != nil {
|
||||
log.Println("Error parsing check_in_out", err)
|
||||
slog.Warn("Error parsing check_in_out")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package paramParser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -10,22 +12,82 @@ type ParamsParser struct {
|
||||
urlParams url.Values
|
||||
}
|
||||
|
||||
func New(_urlParams url.Values) ParamsParser {
|
||||
type NoValueError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e *NoValueError) Error() string {
|
||||
return fmt.Sprintf("No value found for key %s", e.Key)
|
||||
}
|
||||
|
||||
func New(params url.Values) ParamsParser {
|
||||
return ParamsParser{
|
||||
urlParams: _urlParams,
|
||||
urlParams: params,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParamsParser) ParseTimestamp(key string, format string, fallback time.Time) time.Time {
|
||||
paramTimestamp := p.urlParams.Get(key)
|
||||
if paramTimestamp == "" {
|
||||
func (p *ParamsParser) ParseTimestampFallback(key string, format string, fallback time.Time) time.Time {
|
||||
if !p.urlParams.Has(key) {
|
||||
return fallback
|
||||
}
|
||||
paramTimestamp := p.urlParams.Get(key)
|
||||
if timestamp, err := time.Parse(format, paramTimestamp); err == nil {
|
||||
return timestamp
|
||||
} else {
|
||||
slog.Warn("Error parsing HTTP Params to timestamp", slog.Any("key", key), slog.Any("error", err))
|
||||
slog.Warn("Error parsing HTTP Params to time.Time", slog.Any("key", key), slog.Any("error", err))
|
||||
return fallback
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (p *ParamsParser) ParseTimestamp(key string, format string) (time.Time, error) {
|
||||
if !p.urlParams.Has(key) {
|
||||
return time.Time{}, &NoValueError{Key: key}
|
||||
}
|
||||
paramTimestamp := p.urlParams.Get(key)
|
||||
if timestamp, err := time.Parse(format, paramTimestamp); err == nil {
|
||||
return timestamp, nil
|
||||
} else {
|
||||
slog.Debug("Error parsing HTTP Params to time.Time", slog.Any("key", key), slog.Any("error", err))
|
||||
return timestamp, err
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParamsParser) ParseStringFallback(key string, fallback string) string {
|
||||
if !p.urlParams.Has(key) {
|
||||
return fallback
|
||||
}
|
||||
return p.urlParams.Get(key)
|
||||
}
|
||||
|
||||
func (p *ParamsParser) ParseString(key string, fallback string) (string, error) {
|
||||
if !p.urlParams.Has(key) {
|
||||
return "", &NoValueError{Key: key}
|
||||
}
|
||||
return p.urlParams.Get(key), nil
|
||||
}
|
||||
|
||||
func (p *ParamsParser) ParseIntFallback(key string, fallback int) int {
|
||||
if !p.urlParams.Has(key) {
|
||||
return fallback
|
||||
}
|
||||
paramInt := p.urlParams.Get(key)
|
||||
if result, err := strconv.Atoi(paramInt); err == nil {
|
||||
return result
|
||||
} else {
|
||||
slog.Warn("Error parsing HTTP Params to Int", slog.Any("key", key), slog.Any("error", err))
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParamsParser) ParseInt(key string) (int, error) {
|
||||
if !p.urlParams.Has(key) {
|
||||
return 0, &NoValueError{Key: key}
|
||||
}
|
||||
paramInt := p.urlParams.Get(key)
|
||||
if result, err := strconv.Atoi(paramInt); err == nil {
|
||||
return result, nil
|
||||
} else {
|
||||
slog.Debug("Error parsing HTTP Params to Int", slog.Any("key", key), slog.Any("error", err))
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,3 +55,16 @@ func FormatDurationFill(d time.Duration, fill bool) string {
|
||||
func IsSameDate(a, b time.Time) bool {
|
||||
return a.Truncate(24 * time.Hour).Equal(b.Truncate(24 * time.Hour))
|
||||
}
|
||||
|
||||
func GetWorkingDays(startDate, endDate time.Time) int {
|
||||
if endDate.Before(startDate) {
|
||||
return 0
|
||||
}
|
||||
var count int = 0
|
||||
for d := startDate.Truncate(24 * time.Hour); !d.After(endDate); d = d.Add(24 * time.Hour) {
|
||||
if !IsWeekend(d) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -35,3 +36,25 @@ func TestFormatDuration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetWorkingDays(t *testing.T) {
|
||||
testCases := []struct {
|
||||
start string
|
||||
end string
|
||||
days int
|
||||
}{
|
||||
{"2025-10-01", "2025-10-02", 2},
|
||||
{"2025-10-02", "2025-10-01", 0},
|
||||
{"2025-10-01", "2025-10-31", 23},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("WorkingDayTest: %d days", tc.days), func(t *testing.T) {
|
||||
startDate, _ := time.Parse(time.DateOnly, tc.start)
|
||||
endDate, _ := time.Parse(time.DateOnly, tc.end)
|
||||
if GetWorkingDays(startDate, endDate) != tc.days {
|
||||
t.Error("Calculated workdays do not match target")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func main() {
|
||||
server.HandleFunc("/team", endpoints.TeamHandler)
|
||||
server.HandleFunc("/presence", endpoints.TeamPresenceHandler)
|
||||
server.Handle("/pdf", ParamsMiddleware(endpoints.PDFFormHandler))
|
||||
server.HandleFunc("/pdf/generate", endpoints.PDFHandler)
|
||||
server.HandleFunc("/pdf/generate", endpoints.PDFCreateController)
|
||||
server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect))
|
||||
server.Handle("/static/", http.StripPrefix("/static/", fs))
|
||||
|
||||
|
||||
@@ -36,10 +36,10 @@ templ PDFForm(teamMembers []models.User) {
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="grid-sub divide-x-1 responsive">
|
||||
<div class="grid-cell">Direktvorschau oder Download</div>
|
||||
<div class="grid-cell">PDFs Bündeln</div>
|
||||
<div class="grid-cell col-span-3 flex gap-2 flex-col md:flex-row">
|
||||
<button class="btn" type="button" name="action" value="download">Download</button>
|
||||
<button class="btn" type="button" name="action" value="preview" onclick="">Vorschau</button>
|
||||
<button class="btn" type="button" name="action" value="download">Einzeln</button>
|
||||
<button class="btn" type="button" name="action" value="preview" onclick="">Bündel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@ func PDFForm(teamMembers []models.User) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div></div></div><div class=\"grid-sub divide-x-1 responsive\"><div class=\"grid-cell\">Direktvorschau oder Download</div><div class=\"grid-cell col-span-3 flex gap-2 flex-col md:flex-row\"><button class=\"btn\" type=\"button\" name=\"action\" value=\"download\">Download</button> <button class=\"btn\" type=\"button\" name=\"action\" value=\"preview\" onclick=\"\">Vorschau</button></div></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div></div></div><div class=\"grid-sub divide-x-1 responsive\"><div class=\"grid-cell\">PDFs Bündeln</div><div class=\"grid-cell col-span-3 flex gap-2 flex-col md:flex-row\"><button class=\"btn\" type=\"button\" name=\"action\" value=\"download\">Einzeln</button> <button class=\"btn\" type=\"button\" name=\"action\" value=\"preview\" onclick=\"\">Bündel</button></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
30
db.sql
30
db.sql
@@ -176,15 +176,15 @@ sample_bookings AS (
|
||||
(d.work_date + make_time(16, floor(random()*50)::int, 0))::timestamptz AS ts,
|
||||
1 AS anwesenheit_typ
|
||||
FROM days d
|
||||
),
|
||||
ins_anw AS (
|
||||
-- insert only bookings up to now (prevents future times on today)
|
||||
INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id)
|
||||
SELECT ts, card_uid, check_in_out, geraet_id
|
||||
FROM sample_bookings
|
||||
WHERE ts <= NOW()
|
||||
RETURNING 1
|
||||
)
|
||||
|
||||
-- insert only bookings up to now (prevents future times on today)
|
||||
INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id)
|
||||
SELECT ts, card_uid, check_in_out, geraet_id
|
||||
FROM sample_bookings
|
||||
WHERE ts <= NOW()
|
||||
RETURNING 1;
|
||||
|
||||
-- now insert absences (uses the same days CTE)
|
||||
INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum)
|
||||
SELECT
|
||||
@@ -247,15 +247,13 @@ all_bookings AS (
|
||||
SELECT * FROM base_bookings
|
||||
UNION ALL
|
||||
SELECT * FROM pause_bookings
|
||||
),
|
||||
ins_anw AS (
|
||||
INSERT INTO anwesenheit ("timestamp", "card_uid", "check_in_out", "geraet_id", "anwesenheit_typ")
|
||||
SELECT ts, card_uid, check_in_out, geraet_id, 1 as anwesenheit_typ
|
||||
FROM all_bookings
|
||||
WHERE ts <= NOW()
|
||||
ORDER BY work_date, ts
|
||||
RETURNING 1
|
||||
)
|
||||
INSERT INTO anwesenheit ("timestamp", "card_uid", "check_in_out", "geraet_id", "anwesenheit_typ")
|
||||
SELECT ts, card_uid, check_in_out, geraet_id, 1 as anwesenheit_typ
|
||||
FROM all_bookings
|
||||
WHERE ts <= NOW()
|
||||
ORDER BY work_date, ts;
|
||||
|
||||
INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum)
|
||||
SELECT
|
||||
d.card_uid,
|
||||
|
||||
Reference in New Issue
Block a user