22 Commits

Author SHA1 Message Date
23896e4f08 fix: wrong kurzarbeit calculation fixed #80
Some checks failed
Tests / Run Go Tests (push) Failing after 1m5s
2026-02-15 18:16:58 +01:00
7e54800bc3 chore(docs): redid readme + cleanup 2026-02-15 18:16:58 +01:00
61ce5aab3a fix: log verbosity auto/logout to not expose names 2026-02-15 18:16:58 +01:00
1d7b563a6d fix: install script
added dynamic backup folder (fixed #79)
changed if statements
2026-02-15 18:16:58 +01:00
46218f9bca fix: weekbased calculation pdf report
with this change the time calculations for pdf reports should be better
line with the reports send as "week_report"
2026-02-15 18:16:57 +01:00
8911165c4b feat: locking days, if they are submitted and accepted
fixed #76
2026-02-15 18:16:57 +01:00
2d8747c971 fix: refactored sql request for get days + highlight invalid bookings and ignore them for calculation
Some checks failed
Tests / Run Go Tests (push) Failing after 1m14s
fixed #77
2026-02-06 17:54:16 +01:00
38322a64cf fix: time calc errors with only one booking per day + working empty kurzarbeit fixed #74 2026-02-06 17:52:57 +01:00
6b0b8906a9 Merge pull request 'feat: new documentation + project cleanup' (#73) from dev/main into main
All checks were successful
Tests / Run Go Tests (push) Successful in 3m13s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 3m21s
Reviewed-on: #73
2026-01-29 18:56:34 +01:00
fb1cb5d178 Merge branch 'main' into dev/main
All checks were successful
Tests / Run Go Tests (push) Successful in 2m59s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 3m5s
2026-01-29 18:51:07 +01:00
ddaf07f15d fix: dockerfile premission problems
Some checks failed
Arbeitszeitmessung Deploy / Build Webserver (push) Has been cancelled
Tests / Run Go Tests (push) Failing after 1m33s
2026-01-29 18:46:46 +01:00
bec94deaae fix: added templ generate to git actions
Some checks failed
Arbeitszeitmessung Deploy / Build Webserver (push) Has been cancelled
Tests / Run Go Tests (push) Failing after 1m35s
2026-01-29 18:41:18 +01:00
e781f52f6d fix: fixed templ docker version
Some checks failed
Arbeitszeitmessung Deploy / Build Webserver (push) Has been cancelled
Tests / Run Go Tests (push) Failing after 1m2s
2026-01-29 18:34:45 +01:00
ba034f1c33 feat: updated docs and added description to files
Some checks failed
Tests / Run Go Tests (push) Failing after 1m35s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 2m48s
2026-01-29 18:28:28 +01:00
41c34c42cf feat: updated docs to include filestruct
Some checks failed
Tests / Run Go Tests (push) Failing after 2m53s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 3m4s
2026-01-26 21:46:51 +01:00
6998d07c6b fix: css bug after container browser update 2026-01-26 21:46:32 +01:00
a5f5c37225 feat: compile templ files in docker build 2026-01-26 21:45:40 +01:00
6c1ed8eb99 Merge pull request 'fixed problems from install script' (#72) from dev/main into main
All checks were successful
Tests / Run Go Tests (push) Successful in 2m16s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 2m47s
Reviewed-on: #72
2026-01-18 22:55:12 +01:00
fdda0ea669 moved migrations
All checks were successful
Tests / Run Go Tests (push) Successful in 2m7s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 2m45s
2026-01-18 22:50:51 +01:00
c10ab98997 fixed problem, where migrate could not connect to db
Some checks failed
Arbeitszeitmessung Deploy / Build Webserver (push) Failing after 1m14s
Tests / Run Go Tests (push) Successful in 1m48s
2026-01-18 22:42:07 +01:00
8dc8c4eed3 working to fix orangepi db connect
Some checks failed
Tests / Run Go Tests (push) Failing after 30s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 2m11s
2026-01-18 21:34:11 +01:00
3f49da49b6 ad hoc fix
All checks were successful
Tests / Run Go Tests (push) Successful in 2m24s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 2m52s
2026-01-18 20:43:38 +01:00
88 changed files with 1469 additions and 4253 deletions

View File

@@ -6,6 +6,7 @@ on:
- "*" - "*"
branches: branches:
- main - main
- dev/main
jobs: jobs:
webserver: webserver:

View File

@@ -46,6 +46,8 @@ jobs:
key: arbeitszeitmessung-${{ steps.hash-go.outputs.hash }} key: arbeitszeitmessung-${{ steps.hash-go.outputs.hash }}
restore-keys: |- restore-keys: |-
arbeitszeitmessung- arbeitszeitmessung-
- name: Render Templ files
run: cd Backend && go install github.com/a-h/templ/cmd/templ@v0.3.977 && templ generate
- name: Run Go Tests - name: Run Go Tests
run: cd Backend && mkdir .test && go test ./... -coverprofile=.test/coverage.out -json > .test/report.json run: cd Backend && mkdir .test && go test ./... -coverprofile=.test/coverage.out -json > .test/report.json
- name: Verify coverage report exists - name: Verify coverage report exists

2
.gitignore vendored
View File

@@ -40,3 +40,5 @@ atlas.hcl
.scannerwork .scannerwork
Backend/logs Backend/logs
.worktime.txt .worktime.txt
*_templ.go

View File

@@ -1,2 +1,3 @@
db db
Dockerfile Dockerfile
*_templ.go

View File

@@ -1,24 +1,34 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:alpine AS build FROM --platform=$BUILDPLATFORM golang:alpine AS base
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
ENV CGO_ENABLED=0 \ ENV CGO_ENABLED=0 \
GOOS=$TARGETOS \ GOOS=$TARGETOS \
GOARCH=$TARGETARCH GOARCH=$TARGETARCH
FROM base AS fetch-stage
WORKDIR /app WORKDIR /app
COPY go.mod go.sum /app/ COPY go.mod go.sum /app/
RUN go mod download && go mod verify RUN go mod download && go mod verify
COPY . . FROM ghcr.io/a-h/templ:v0.3.977 AS generate-stage
COPY --chown=65532:65532 --chmod=755 . /app
WORKDIR /app
RUN ["templ", "generate"]
FROM base AS build
COPY --from=generate-stage /app /app
WORKDIR /app
RUN go build -o server . RUN go build -o server .
FROM alpine:3.22 FROM alpine:3.22
RUN apk add --no-cache tzdata typst RUN apk add --no-cache tzdata typst
WORKDIR /app WORKDIR /app
COPY --from=build /app/server /app/server COPY --from=build /app/server /app/server
COPY ./doc/static /doc/static
COPY ./doc/templates /doc/templates COPY migrations /app/migrations
COPY doc /doc
COPY /static /app/static COPY /static /app/static
ENTRYPOINT ["./server"] ENTRYPOINT ["./server"]

View File

@@ -1,5 +1,8 @@
package main package main
// this file handles the db connection and the
// db migration
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
@@ -8,6 +11,7 @@ import (
"log/slog" "log/slog"
"github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file" _ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/lib/pq" _ "github.com/lib/pq"
@@ -20,21 +24,34 @@ func OpenDatabase() (models.IDatabase, error) {
dbPassword := helper.GetEnv("POSTGRES_API_PASS", "password") dbPassword := helper.GetEnv("POSTGRES_API_PASS", "password")
dbTz := helper.GetEnv("TZ", "Europe/Berlin") dbTz := helper.GetEnv("TZ", "Europe/Berlin")
connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=%s", dbUser, dbPassword, dbHost, dbName, dbTz) connStr := fmt.Sprintf(
"host=%s user=%s dbname=%s password=%s sslmode=disable TimeZone=%s",
dbHost, dbUser, dbName, dbPassword, dbTz)
return sql.Open("postgres", connStr) return sql.Open("postgres", connStr)
} }
func Migrate() error { func Migrate() error {
dbHost := helper.GetEnv("POSTGRES_HOST", "localhost") dbHost := helper.GetEnv("POSTGRES_HOST", "localhost")
dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung") dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung")
// dbUser := helper.GetEnv("POSTGRES_USER", "api_nutzer")
dbPassword := helper.GetEnv("POSTGRES_PASSWORD", "password") dbPassword := helper.GetEnv("POSTGRES_PASSWORD", "password")
dbTz := helper.GetEnv("TZ", "Europe/Berlin") dbTz := helper.GetEnv("TZ", "Europe/Berlin")
migrations := helper.GetEnv("MIGRATIONS_PATH", "../migrations") connStr := fmt.Sprintf(
"host=%s user=%s dbname=%s password=%s sslmode=disable TimeZone=%s",
dbHost, "migrate", dbName, dbPassword, dbTz)
connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=%s", "migrate", dbPassword, dbHost, dbName, dbTz) db, err := sql.Open("postgres", connStr)
m, err := migrate.New(fmt.Sprintf("file://%s", migrations), connStr) if err != nil {
return err
}
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return err
}
m, err := migrate.NewWithDatabaseInstance("file:///app/migrations", "postgres", driver)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,5 +1,15 @@
// endpoints contains all http endpoints
// for more complex endpoints the *Handler function is executed first
// by the main programm and it will then run other functions as needed
//
// the filenames represent the route/url for the given endpoint
// when "-" is a "/" so this file is server at "/auto/feiertage"
package endpoints package endpoints
// this endpoint will be called by crontab and generates the public holidays for a given year
// after that manually added holidays with a "wiederholen" flag are copied over to the new year
import ( import (
"arbeitszeitmessung/helper/paramParser" "arbeitszeitmessung/helper/paramParser"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"

View File

@@ -1,5 +1,10 @@
package endpoints package endpoints
// this served as "/auto/kurzarbeit" will add a booking to every kurzarbeitstag
// to make them reach the full lenght of workday.
//
// right now this is not in use because the time is calculated
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser" "arbeitszeitmessung/helper/paramParser"
@@ -58,8 +63,8 @@ func fillKurzarbeit(r *http.Request, w http.ResponseWriter) {
workday, _ := day.(*models.WorkDay) workday, _ := day.(*models.WorkDay)
lastBookingTime := workday.Bookings[len(workday.Bookings)-1].Timestamp lastBookingTime := workday.Bookings[len(workday.Bookings)-1].Timestamp
kurzarbeitBegin := (*models.Booking).New(nil, user.CardUID, 0, 1, bookingTypeKurzarbeit.Id) kurzarbeitBegin := (*models.Booking).NewBooking(nil, user.CardUID, 0, 1, bookingTypeKurzarbeit.Id)
kurzarbeitEnd := (*models.Booking).New(nil, user.CardUID, 0, 2, bookingTypeKurzarbeit.Id) kurzarbeitEnd := (*models.Booking).NewBooking(nil, user.CardUID, 0, 2, bookingTypeKurzarbeit.Id)
kurzarbeitBegin.Timestamp = lastBookingTime.Add(time.Minute) kurzarbeitBegin.Timestamp = lastBookingTime.Add(time.Minute)
kurzarbeitEnd.Timestamp = lastBookingTime.Add(worktimeKurzarbeit) kurzarbeitEnd.Timestamp = lastBookingTime.Add(worktimeKurzarbeit)

View File

@@ -1,5 +1,8 @@
package endpoints package endpoints
// this endpoint served at "/auto/logout" will be executed by crontab
// and will log out all users that are currently still logged in
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
@@ -32,7 +35,7 @@ func autoLogout(w http.ResponseWriter) {
fmt.Printf("Error logging out user %v\n", err) fmt.Printf("Error logging out user %v\n", err)
} else { } else {
loggedOutUsers = append(loggedOutUsers, user) loggedOutUsers = append(loggedOutUsers, user)
log.Printf("Automaticaly logged out user %s, %s ", user.Name, user.Vorname) log.Printf("Automaticaly logged out user %d ", user.PersonalNummer)
} }
} }

View File

@@ -1,5 +1,8 @@
package endpoints package endpoints
// this endpoint served at "/pdf/create" accepts the contents from the pdf form
// and renders a pdf according to this form
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser" "arbeitszeitmessung/helper/paramParser"
@@ -18,66 +21,7 @@ import (
const DE_DATE string = "02.01.2006" const DE_DATE string = "02.01.2006"
const FILE_YEAR_MONTH string = "2006_01" const FILE_YEAR_MONTH string = "2006_01"
func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) { var PDF_DIRECTORY = helper.GetEnv("PDF_PATH", "/doc/") // TODO
var typstDays []typstDay
for _, day := range days {
var thisTypstDay typstDay
work, pause, overtime := day.GetTimes(u, models.WorktimeBaseDay, false)
workVirtual := day.GetWorktime(u, models.WorktimeBaseDay, true)
overtime = workVirtual - u.ArbeitszeitProWocheFrac(0.2)
thisTypstDay.Date = day.Date().Format(DE_DATE)
thisTypstDay.Worktime = helper.FormatDurationFill(workVirtual, true)
thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true)
thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true)
thisTypstDay.IsFriday = day.Date().Weekday() == time.Friday
if workVirtual > work {
thisTypstDay.Kurzarbeit = helper.FormatDurationFill(workVirtual-work, true)
} else {
thisTypstDay.Kurzarbeit = helper.FormatDurationFill(0, true)
}
thisTypstDay.DayParts = convertDayToTypstDayParts(day, u)
typstDays = append(typstDays, thisTypstDay)
}
return typstDays, nil
}
func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDayPart {
var typstDayParts []typstDayPart
switch day.Type() {
case models.DayTypeWorkday:
workDay, _ := day.(*models.WorkDay)
for i := 0; i < len(workDay.Bookings); i += 2 {
var typstDayPart typstDayPart
typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04")
if i+1 < len(workDay.Bookings) {
typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04")
} else {
typstDayPart.BookingTo = workDay.Bookings[i].Timestamp.Format("15:04")
}
typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name
typstDayPart.IsWorkDay = true
typstDayParts = append(typstDayParts, typstDayPart)
}
if day.IsKurzArbeit() && len(workDay.Bookings) > 0 {
tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user)
typstDayParts = append(typstDayParts, typstDayPart{
BookingFrom: tsFrom.Format("15:04"),
BookingTo: tsTo.Format("15:04"),
WorkType: "Kurzarbeit",
IsWorkDay: true,
})
}
case models.DayTypeCompound:
for _, c := range day.(*models.CompoundDay).DayParts {
typstDayParts = append(typstDayParts, convertDayToTypstDayParts(c, user)...)
}
default:
typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: day.ToString()})
}
return typstDayParts
}
func PDFCreateController(w http.ResponseWriter, r *http.Request) { func PDFCreateController(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r) helper.RequiresLogin(Session, w, r)
@@ -98,6 +42,7 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
return return
} }
if !helper.IsDebug() {
n := 0 n := 0
for _, e := range employes { for _, e := range employes {
if user.IsSuperior(e) { if user.IsSuperior(e) {
@@ -106,6 +51,7 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
} }
} }
employes = employes[:n] employes = employes[:n]
}
reportData := createReports(employes, startDate) reportData := createReports(employes, startDate)
@@ -116,8 +62,9 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
slog.Warn("Could not create pdf report", slog.Any("Error", err)) slog.Warn("Could not create pdf report", slog.Any("Error", err))
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
w.Header().Set("Content-type", "application/pdf") w.Header().Set("Content-type", "application/pdf")
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=Monatsabrechnung_%s", startDate.Format(FILE_YEAR_MONTH))) w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=Monatsabrechnung_%s.pdf", startDate.Format(FILE_YEAR_MONTH)))
output.WriteTo(w) output.WriteTo(w)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
case "download": case "download":
@@ -128,11 +75,12 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
} }
output, err := zipPfd(pdfReports, &reportData) output, err := zipPfd(pdfReports, &reportData)
if err != nil { if err != nil {
slog.Warn("Could not create pdf report", slog.Any("Error", err)) slog.Warn("Could not zip pdf reports", slog.Any("Error", err))
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
w.Header().Set("Content-type", "application/zip") w.Header().Set("Content-type", "application/zip")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachement; filename=Monatsabrechnung_%s", startDate.Format(FILE_YEAR_MONTH))) w.Header().Set("Content-Disposition", fmt.Sprintf("attachement; filename=Monatsabrechnung_%s.zip", startDate.Format(FILE_YEAR_MONTH)))
output.WriteTo(w) output.WriteTo(w)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
@@ -142,6 +90,75 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
} }
} }
func convertDaysToTypst(days []models.IWorkDay, u models.User, weekbase models.WorktimeBase) ([]typstDay, error) {
var typstDays []typstDay
for i, day := range days {
if !day.IsSubmittedAndAccepted() && !helper.IsDebug() {
continue
}
var thisTypstDay typstDay
workVirtual, pause, overtime := day.GetTimes(u, weekbase, true)
overtime = workVirtual - u.ArbeitszeitProWocheFrac(0.2)
if day.Type() == models.DayTypeHoliday {
// workVirtual = 0
overtime = 0
}
thisTypstDay.Date = day.Date().Format(DE_DATE)
thisTypstDay.Worktime = helper.FormatDurationFill(workVirtual, true)
thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true)
thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true)
thisTypstDay.IsFriday = i == len(days)-1
if work := day.GetWorktime(u, weekbase, false); workVirtual > work {
thisTypstDay.Kurzarbeit = helper.FormatDurationFill(workVirtual-work, true)
} else {
thisTypstDay.Kurzarbeit = helper.FormatDurationFill(0, true)
}
thisTypstDay.DayParts = convertDayToTypstDayParts(day, u, weekbase)
typstDays = append(typstDays, thisTypstDay)
}
return typstDays, nil
}
func convertDayToTypstDayParts(day models.IWorkDay, user models.User, weekBase models.WorktimeBase) []typstDayPart {
var typstDayParts []typstDayPart
switch day.Type() {
case models.DayTypeWorkday:
workDay, _ := day.(*models.WorkDay)
for i := 0; i < len(workDay.Bookings); i += 2 {
var typstDayPart typstDayPart
typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04")
if i+1 < len(workDay.Bookings) {
typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04")
} else {
typstDayPart.BookingTo = workDay.Bookings[i].Timestamp.Format("15:04")
}
typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name
typstDayPart.IsWorkDay = true
typstDayParts = append(typstDayParts, typstDayPart)
}
if day.IsKurzArbeit() {
tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user, weekBase)
typstDayParts = append(typstDayParts, typstDayPart{
BookingFrom: tsFrom.Format("15:04"),
BookingTo: tsTo.Format("15:04"),
WorkType: "Kurzarbeit",
IsWorkDay: true,
})
}
case models.DayTypeCompound:
for _, c := range day.(*models.CompoundDay).DayParts {
typstDayParts = append(typstDayParts, convertDayToTypstDayParts(c, user, weekBase)...)
}
default:
typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: day.ToString()})
}
return typstDayParts
}
func createReports(employes []models.User, startDate time.Time) []typstData { func createReports(employes []models.User, startDate time.Time) []typstData {
startDate = helper.GetFirstOfMonth(startDate) startDate = helper.GetFirstOfMonth(startDate)
endDate := startDate.AddDate(0, 1, -1) endDate := startDate.AddDate(0, 1, -1)
@@ -158,37 +175,49 @@ func createReports(employes []models.User, startDate time.Time) []typstData {
} }
func createEmployeReport(employee models.User, startDate, endDate time.Time) (typstData, error) { func createEmployeReport(employee models.User, startDate, endDate time.Time) (typstData, error) {
publicHolidays, err := models.GetHolidaysFromTo(startDate, endDate) // publicHolidays, _ := models.GetHolidaysFromTo(startDate, endDate)
targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)-len(publicHolidays)) targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)) //-len(publicHolidays)
workDaysThisMonth := models.GetDays(employee, startDate, endDate.AddDate(0, 0, 1), false) mondaysThisMonth := helper.GetMondays(helper.GenerateDateRange(startDate, endDate), false)
slog.Debug("Baseline Working hours", "targetHours", targetHoursThisMonth.Hours())
var weeks []models.WorkWeek
var workHours, kurzarbeitHours time.Duration var workHours, kurzarbeitHours time.Duration
for _, day := range workDaysThisMonth {
tmpvirtualHours := day.GetWorktime(employee, models.WorktimeBaseDay, true)
tmpactualHours := day.GetWorktime(employee, models.WorktimeBaseDay, false)
if day.IsKurzArbeit() && tmpvirtualHours > tmpactualHours {
slog.Debug("Adding kurzarbeit to workday", "day", day.Date())
kurzarbeitHours += tmpvirtualHours - tmpactualHours
}
workHours += tmpvirtualHours
}
worktimeBalance := workHours - targetHoursThisMonth
typstDays, err := convertDaysToTypst(workDaysThisMonth, employee) for _, monday := range mondaysThisMonth {
var week models.WorkWeek
if monday.After(startDate) {
week = models.NewWorkWeekSimple(employee, monday, true)
} else if startDate.Sub(monday) < time.Hour*24*6 {
week = models.NewWorkWeek(employee, startDate, monday.Add(6*24*time.Hour), true)
}
workHours += week.WorktimeVirtual
kurzarbeitHours += week.Kurzarbeit
weeks = append(weeks, week)
}
monthOvertime := workHours - targetHoursThisMonth
totalOvertime, err := employee.GetReportedOvertime(endDate)
var typstDays []typstDay
for _, week := range weeks {
weekTypstDays, err := convertDaysToTypst(week.Days, employee, week.WeekBase)
if err != nil { if err != nil {
slog.Warn("Failed to convert to days", slog.Any("error", err)) slog.Error("Error converting days into typst", "error", err)
return typstData{}, err continue
}
typstDays = append(typstDays, weekTypstDays...)
}
if err != nil {
slog.Error("Cannot retrieve total Overtime", "Error", err)
} }
metadata := typstMetadata{ metadata := typstMetadata{
EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name), EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name),
TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)), TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)),
Overtime: helper.FormatDurationFill(worktimeBalance, true), Overtime: helper.FormatDurationFill(monthOvertime, true),
WorkTime: helper.FormatDurationFill(workHours, true), WorkTime: helper.FormatDurationFill(workHours, true),
Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true), Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true),
OvertimeTotal: "", OvertimeTotal: helper.FormatDurationFill(totalOvertime+monthOvertime, true),
CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"),
} }
return typstData{Meta: metadata, Days: typstDays, FileName: fmt.Sprintf("%s_%s.pdf", startDate.Format(FILE_YEAR_MONTH), employee.Name)}, nil return typstData{Meta: metadata, Days: typstDays, FileName: fmt.Sprintf("%s_%s.pdf", startDate.Format(FILE_YEAR_MONTH), employee.Name)}, nil
@@ -199,8 +228,7 @@ func renderPDFSingle(data []typstData) (bytes.Buffer, error) {
var output bytes.Buffer var output bytes.Buffer
typstCLI := typst.CLI{ typstCLI := typst.CLI{
WorkingDirectory: "/doc/", WorkingDirectory: PDF_DIRECTORY,
// ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
} }
if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil { if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil {
@@ -227,8 +255,7 @@ func renderPDFMulti(data []typstData) ([]bytes.Buffer, error) {
var outputMulti []bytes.Buffer var outputMulti []bytes.Buffer
typstRender := typst.CLI{ typstRender := typst.CLI{
WorkingDirectory: "/doc/", WorkingDirectory: PDF_DIRECTORY,
// ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
} }
for _, d := range data { for _, d := range data {
@@ -270,6 +297,16 @@ func zipPfd(pdfReports []bytes.Buffer, reportData *[]typstData) (bytes.Buffer, e
return zipOutput, err return zipOutput, err
} }
func lenWorkDays(workDays []models.IWorkDay) int {
var lenght int
for _, day := range workDays {
if !day.IsEmpty() || day.IsKurzArbeit() {
lenght += 1
}
}
return lenght
}
type typstMetadata struct { type typstMetadata struct {
TimeRange string `json:"time-range"` TimeRange string `json:"time-range"`
EmployeeName string `json:"employee-name"` EmployeeName string `json:"employee-name"`

View File

@@ -1,5 +1,7 @@
package endpoints package endpoints
// this endpoint served at "/pdf" handles the rendering of the pdf form
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"

View File

@@ -1,5 +1,7 @@
package endpoints package endpoints
// this endpoint served at "/team/presence" shows the presence page
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
@@ -8,7 +10,7 @@ import (
"net/http" "net/http"
) )
func TeamPresenceHandler(w http.ResponseWriter, r *http.Request) { func PresenceHandler(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r) helper.RequiresLogin(Session, w, r)
helper.SetCors(w) helper.SetCors(w)
switch r.Method { switch r.Method {

View File

@@ -1,5 +1,8 @@
package endpoints package endpoints
// this endpoint served at "/team/report" handles the report page
// and also the submission/change of reports
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser" "arbeitszeitmessung/helper/paramParser"
@@ -12,7 +15,7 @@ import (
"time" "time"
) )
func TeamHandler(w http.ResponseWriter, r *http.Request) { func ReportHandler(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r) helper.RequiresLogin(Session, w, r)
switch r.Method { switch r.Method {
case http.MethodPost: case http.MethodPost:
@@ -39,7 +42,7 @@ func submitReport(w http.ResponseWriter, r *http.Request) {
return return
} }
workWeek := models.NewWorkWeek(user, weekTs, true) workWeek := models.NewWorkWeekSimple(user, weekTs, true)
switch r.FormValue("method") { switch r.FormValue("method") {
case "send": case "send":
@@ -67,7 +70,7 @@ func showWeeks(w http.ResponseWriter, r *http.Request) {
submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission()) submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission())
lastSub := helper.GetMonday(submissionDate) lastSub := helper.GetMonday(submissionDate)
userWeek := models.NewWorkWeek(user, lastSub, true) userWeek := models.NewWorkWeekSimple(user, lastSub, true)
var workWeeks []models.WorkWeek var workWeeks []models.WorkWeek
teamMembers, err := user.GetTeamMembers() teamMembers, err := user.GetTeamMembers()
@@ -76,5 +79,5 @@ func showWeeks(w http.ResponseWriter, r *http.Request) {
workWeeks = append(workWeeks, weeks...) workWeeks = append(workWeeks, weeks...)
} }
// isRunningWeek := time.Since(lastSub) < 24*5*time.Hour //the last submission is this week and cannot be send yet // isRunningWeek := time.Since(lastSub) < 24*5*time.Hour //the last submission is this week and cannot be send yet
templates.TeamPage(workWeeks, userWeek).Render(r.Context(), w) templates.ReportPage(workWeeks, userWeek).Render(r.Context(), w)
} }

View File

@@ -1,5 +1,8 @@
package endpoints package endpoints
// this endpoint served at "/time/create" is for the esp api and creates bookings
// either via HTTP GET or HTTP PUT
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"

View File

@@ -1,5 +1,9 @@
package endpoints package endpoints
// this endpoint served at "/time" handles the time page
// this includes normal show + creation of bookings from the webpage +
// edit functionality
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser" "arbeitszeitmessung/helper/paramParser"
@@ -96,7 +100,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
} }
aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, true) aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, true)
} }
if reportedOvertime, err := user.GetReportedOvertime(); err == nil { if reportedOvertime, err := user.GetReportedOvertime(time.Now()); err == nil {
user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute) user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute)
} else { } else {
log.Println("Cannot calculate overtime: ", err) log.Println("Cannot calculate overtime: ", err)
@@ -158,7 +162,7 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
return return
} }
newBooking := (*models.Booking).New(nil, user.CardUID, 0, int16(check_in_out), 1) newBooking := (*models.Booking).NewBooking(nil, user.CardUID, 0, int16(check_in_out), 1)
newBooking.Timestamp = timestamp newBooking.Timestamp = timestamp
if newBooking.Verify() { if newBooking.Verify() {
err = newBooking.InsertWithTimestamp() err = newBooking.InsertWithTimestamp()

View File

@@ -0,0 +1,17 @@
package endpoints
// this endpoint server at "/user/login" will show the login page or
// directly login the user based on the http method used
import "net/http"
func LoginHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
showLoginPage(w, r, true, "")
case http.MethodPost:
loginUser(w, r)
default:
http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed)
}
}

View File

@@ -1,74 +0,0 @@
package endpoints
import (
"arbeitszeitmessung/models"
"arbeitszeitmessung/templates"
"context"
"log"
"net/http"
"strconv"
"time"
"github.com/alexedwards/scs/v2"
)
var Session *scs.SessionManager
func CreateSessionManager(lifetime time.Duration) *scs.SessionManager {
Session = scs.New()
Session.Lifetime = lifetime
return Session
}
func showLoginPage(w http.ResponseWriter, r *http.Request, success bool, errorMsg string) {
r = r.WithContext(context.WithValue(r.Context(), "session", Session))
if Session.Exists(r.Context(), "user") {
http.Redirect(w, r, "/time", http.StatusSeeOther)
}
templates.LoginPage(success, errorMsg).Render(r.Context(), w)
}
func loginUser(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
log.Println("Error parsing form!", err)
showLoginPage(w, r, false, "Internal error!")
return
}
_personal_nummer := r.FormValue("personal_nummer")
if _personal_nummer == "" {
log.Println("No personal_nummer provided!")
showLoginPage(w, r, false, "Keine Personalnummer gesetzt.")
return
}
personal_nummer, err := strconv.Atoi(_personal_nummer)
if err != nil {
log.Println("Cannot parse personal nubmer!")
showLoginPage(w, r, false, "Personalnummer ist nicht valide gesetzt.")
return
}
user, err := models.GetUserByPersonalNr(personal_nummer)
if err != nil {
log.Println("No user found under this personal number!", err)
showLoginPage(w, r, false, "Nutzer unter dieser Personalnummer nicht gefunden.")
return
}
password := r.FormValue("password")
if user.Login(password) {
log.Printf("New succesfull user login from %s %s (%d)!\n", user.Vorname, user.Name, user.PersonalNummer)
Session.Put(r.Context(), "user", user.PersonalNummer)
Session.Commit(r.Context())
http.Redirect(w, r, "/time", http.StatusSeeOther) //with this browser always uses GET
}
showLoginPage(w, r, false, "")
}
func logoutUser(w http.ResponseWriter, r *http.Request) {
log.Println("Loggin out user!")
err := Session.Destroy(r.Context())
if err != nil {
log.Println("Error destroying session!", err)
}
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
}

View File

@@ -1,6 +1,11 @@
package endpoints package endpoints
// this endpoint server at "/user/settings" will show the settings page
// depeding on which action is taken the user will be logged out or
// the password will be changed
import ( import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"arbeitszeitmessung/templates" "arbeitszeitmessung/templates"
"context" "context"
@@ -8,6 +13,23 @@ import (
"net/http" "net/http"
) )
func UserSettingsHandler(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r)
switch r.Method {
case http.MethodGet:
showUserPage(w, r, 0)
case http.MethodPost:
switch r.FormValue("action") {
case "change-pass":
changePassword(w, r)
case "logout-user":
logoutUser(w, r)
}
default:
http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed)
}
}
// change user password and store salted hash in db // change user password and store salted hash in db
func changePassword(w http.ResponseWriter, r *http.Request) { func changePassword(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm() err := r.ParseForm()

View File

@@ -1,8 +1,18 @@
package endpoints package endpoints
// this is not directly an endpoint as it servers all requests for "/user"
// and routes the furter to "login", "logout", and "settings"
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/models"
"arbeitszeitmessung/templates"
"context"
"log"
"net/http" "net/http"
"strconv"
"time"
"github.com/alexedwards/scs/v2"
) )
func UserHandler(w http.ResponseWriter, r *http.Request) { func UserHandler(w http.ResponseWriter, r *http.Request) {
@@ -16,31 +26,63 @@ func UserHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
func LoginHandler(w http.ResponseWriter, r *http.Request) { var Session *scs.SessionManager
switch r.Method {
case http.MethodGet: func CreateSessionManager(lifetime time.Duration) *scs.SessionManager {
showLoginPage(w, r, true, "") Session = scs.New()
case http.MethodPost: Session.Lifetime = lifetime
loginUser(w, r) return Session
default:
http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed)
}
} }
func UserSettingsHandler(w http.ResponseWriter, r *http.Request) { func showLoginPage(w http.ResponseWriter, r *http.Request, success bool, errorMsg string) {
helper.RequiresLogin(Session, w, r) r = r.WithContext(context.WithValue(r.Context(), "session", Session))
if Session.Exists(r.Context(), "user") {
http.Redirect(w, r, "/time", http.StatusSeeOther)
}
templates.LoginPage(success, errorMsg).Render(r.Context(), w)
}
switch r.Method { func loginUser(w http.ResponseWriter, r *http.Request) {
case http.MethodGet: err := r.ParseForm()
showUserPage(w, r, 0) if err != nil {
case http.MethodPost: log.Println("Error parsing form!", err)
switch r.FormValue("action") { showLoginPage(w, r, false, "Internal error!")
case "change-pass": return
changePassword(w, r)
case "logout-user":
logoutUser(w, r)
} }
default: _personal_nummer := r.FormValue("personal_nummer")
http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) if _personal_nummer == "" {
log.Println("No personal_nummer provided!")
showLoginPage(w, r, false, "Keine Personalnummer gesetzt.")
return
} }
personal_nummer, err := strconv.Atoi(_personal_nummer)
if err != nil {
log.Println("Cannot parse personal nubmer!")
showLoginPage(w, r, false, "Personalnummer ist nicht valide gesetzt.")
return
}
user, err := models.GetUserByPersonalNr(personal_nummer)
if err != nil {
log.Println("No user found under this personal number!", err)
showLoginPage(w, r, false, "Nutzer unter dieser Personalnummer nicht gefunden.")
return
}
password := r.FormValue("password")
if user.Login(password) {
log.Printf("New succesfull user login from %s %s (%d)!\n", user.Vorname, user.Name, user.PersonalNummer)
Session.Put(r.Context(), "user", user.PersonalNummer)
Session.Commit(r.Context())
http.Redirect(w, r, "/time", http.StatusSeeOther) //with this browser always uses GET
}
showLoginPage(w, r, false, "")
}
func logoutUser(w http.ResponseWriter, r *http.Request) {
log.Println("Loggin out user!")
err := Session.Destroy(r.Context())
if err != nil {
log.Println("Error destroying session!", err)
}
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
} }

View File

@@ -1,3 +1,6 @@
// Helper package to create audit logs in the logs folder
// these are by the time only generated, when a booking is
// changed on the /time page
package logs package logs
import ( import (

View File

@@ -1,3 +1,7 @@
// this package contains different functions to parse URL.Values or Form.Values
// into other datatypes see functionname
// on fail the funktions either return a fallback or error
package paramParser package paramParser
import ( import (

View File

@@ -1,9 +0,0 @@
package helper
func GetFirst[T, U any](val T, _ U) T {
return val
}
func GetSecond[T, U any](_ T, val U) U {
return val
}

View File

@@ -1,47 +0,0 @@
package helper
import "testing"
func TestGetFirst(t *testing.T) {
tests := []struct {
name string
a any
b any
want any
}{
{"ints", 10, 20, 10},
{"strings", "first", "second", "first"},
{"mixed", "abc", 123, "abc"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetFirst(tt.a, tt.b)
if got != tt.want {
t.Errorf("GetFirst(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want)
}
})
}
}
func TestGetSecond(t *testing.T) {
tests := []struct {
name string
a any
b any
want any
}{
{"ints", 10, 20, 20},
{"strings", "first", "second", "second"},
{"mixed", "abc", 123, 123},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetSecond(tt.a, tt.b)
if got != tt.want {
t.Errorf("GetSecond(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want)
}
})
}
}

View File

@@ -1,5 +1,7 @@
package helper package helper
// different system helpers for environment variables + cache
import ( import (
"os" "os"
"time" "time"
@@ -18,6 +20,10 @@ func GetEnv(key, fallback string) string {
return fallback return fallback
} }
func IsDebug() bool {
return GetEnv("GO_ENV", "production") == "debug"
}
type CacheItem struct { type CacheItem struct {
value any value any
expiration time.Time expiration time.Time

View File

@@ -1,7 +1,10 @@
package helper package helper
// time helpers
import ( import (
"fmt" "fmt"
"slices"
"time" "time"
) )
@@ -16,6 +19,64 @@ func GetMonday(ts time.Time) time.Time {
return ts return ts
} }
func GetMondays(allDays []time.Time, onlyInRange bool) []time.Time {
var mondays []time.Time
var start, end time.Time
for _, day := range allDays {
mondays = append(mondays, GetMonday(day))
if start.IsZero() || day.Before(start) {
start = day
}
if end.IsZero() || day.After(end) {
end = day
}
}
mondays = slices.Compact(mondays)
if onlyInRange {
return DaysInRange(mondays, start, end)
}
return mondays
}
func DaysInRange(days []time.Time, startDate, endDate time.Time) []time.Time {
filtered := []time.Time{}
startDate = startDate.Add(-time.Minute)
endDate = endDate.Add(time.Minute)
for _, day := range days {
if day.After(startDate) && day.Before(endDate) {
filtered = append(filtered, day)
}
}
return filtered
}
func IsMonday(day time.Time) bool {
return day.Weekday() == time.Monday
}
// GenerateDateRange returns a slice of all dates between start and end (inclusive).
func GenerateDateRange(start, end time.Time) []time.Time {
var dates []time.Time
// Ensure start is before or equal to end
if start.After(end) {
return dates
}
// Normalize times to midnight
current := start.Truncate(time.Hour * 24)
end = end.Truncate(time.Hour * 24)
for !current.After(end) {
dates = append(dates, current)
current = current.AddDate(0, 0, 1) // Add one day
}
return dates
}
func GetFirstOfMonth(ts time.Time) time.Time { func GetFirstOfMonth(ts time.Time) time.Time {
if ts.Day() > 1 { if ts.Day() > 1 {
return ts.AddDate(0, 0, -(ts.Day() - 1)) return ts.AddDate(0, 0, -(ts.Day() - 1))
@@ -27,11 +88,6 @@ func IsWeekend(ts time.Time) bool {
return ts.Weekday() == time.Saturday || ts.Weekday() == time.Sunday return ts.Weekday() == time.Saturday || ts.Weekday() == time.Sunday
} }
func GetKW(t time.Time) int {
_, kw := t.ISOWeek()
return kw
}
func FormatDuration(d time.Duration) string { func FormatDuration(d time.Duration) string {
return FormatDurationFill(d, false) return FormatDurationFill(d, false)
} }

View File

@@ -26,6 +26,101 @@ func TestGetMonday(t *testing.T) {
} }
} }
func TestIsMonday_ReturnsTrueForMonday(t *testing.T) {
monday := time.Date(2023, 4, 3, 0, 0, 0, 0, time.UTC)
if !IsMonday(monday) {
t.Errorf("Expected IsMonday to return true for Monday, got false")
}
}
func TestIsMonday_ReturnsFalseForNonMonday(t *testing.T) {
tuesday := time.Date(2023, 4, 4, 0, 0, 0, 0, time.UTC)
if IsMonday(tuesday) {
t.Errorf("Expected IsMonday to return false for Tuesday, got true")
}
}
func TestGenerateDateRange(t *testing.T) {
start := time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)
end := time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC)
dates := GenerateDateRange(start, end)
if len(dates) != 3 {
t.Fatalf("expected 3 dates, got %d", len(dates))
}
expected := []string{"2026-02-09", "2026-02-10", "2026-02-11"}
for i, d := range dates {
got := d.Format("2006-01-02")
if got != expected[i] {
t.Errorf("expected %s, got %s", expected[i], got)
}
}
}
func TestGetMondays_ReturnsOnlyMondays(t *testing.T) {
startDate := time.Date(2026, 01, 01, 0, 0, 0, 0, time.UTC)
endDate := time.Date(2026, 01, 31, 0, 0, 0, 0, time.UTC)
daysInMonth := GenerateDateRange(startDate, endDate)
result := GetMondays(daysInMonth, false)
if len(result) < 5 {
t.Errorf("Expected 5 monday, got %d", len(result))
} else if len(result) > 5 {
t.Errorf("Expected 5 monday, got %d", len(result))
}
if result[0] != time.Date(2025, 12, 29, 0, 0, 0, 0, time.UTC) {
t.Errorf("Expected first monday to be %v, got %v", "2025-12-29", result[0])
}
}
func TestGetMondays_ReturnsOnlyMondaysInRange(t *testing.T) {
startDate := time.Date(2026, 01, 01, 0, 0, 0, 0, time.UTC)
endDate := time.Date(2026, 01, 31, 0, 0, 0, 0, time.UTC)
daysInMonth := GenerateDateRange(startDate, endDate)
result := GetMondays(daysInMonth, true)
if len(result) < 4 {
t.Errorf("Expected 4 monday, got %d", len(result))
} else if len(result) > 4 {
t.Errorf("Expected 4 monday, got %d", len(result))
}
if result[0] != time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC) {
t.Errorf("Expected first monday to be %v, got %v", "2026-01-05", result[0])
}
}
func TestDaysInRange(t *testing.T) {
days := []time.Time{
time.Date(2023, 4, 3, 0, 0, 0, 0, time.UTC), // Tuesday
time.Date(2023, 4, 4, 0, 0, 0, 0, time.UTC), // Wednesday
time.Date(2023, 4, 5, 0, 0, 0, 0, time.UTC), // Thursday
time.Date(2023, 4, 6, 0, 0, 0, 0, time.UTC), // Friday
}
start := time.Date(2023, 4, 3, 0, 0, 0, 0, time.UTC)
end := time.Date(2023, 4, 5, 0, 0, 0, 0, time.UTC)
daysInRange := DaysInRange(days, start, end)
if len(daysInRange) != 3 {
t.Errorf("Expected 3 days in range, got %d", len(daysInRange))
}
if daysInRange[0] != days[0] {
t.Errorf("Expected first day in range to be %v, got %v", days[0], daysInRange[0])
}
if daysInRange[2] != days[2] {
t.Errorf("Expected third day in range to be %v, got %v", days[2], daysInRange[2])
}
}
func TestFormatDurationFill(t *testing.T) { func TestFormatDurationFill(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
@@ -132,41 +227,3 @@ func TestFormatGermanDayOfWeek(t *testing.T) {
}) })
} }
} }
func TestGetKW(t *testing.T) {
tests := []struct {
name string
date time.Time
want int
}{
{
name: "First week of year",
date: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC), // Monday
want: 1,
},
{
name: "Middle of year",
date: time.Date(2023, 6, 15, 0, 0, 0, 0, time.UTC),
want: 24,
},
{
name: "Last week of year",
date: time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC),
want: 52,
},
{
name: "ISO week crossing into next year",
date: time.Date(2020, 12, 31, 0, 0, 0, 0, time.UTC),
want: 53,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetKW(tt.date)
if got != tt.want {
t.Errorf("GetKW(%v) = %d, want %d", tt.date, got, tt.want)
}
})
}
}

View File

@@ -1,12 +1,6 @@
package helper package helper
import "time" // type conversion
type TimeFormValue struct {
TsFrom time.Time
TsTo time.Time
CardUID string
}
func BoolToInt(b bool) int { func BoolToInt(b bool) int {
var i int = 0 var i int = 0

View File

@@ -1,5 +1,7 @@
package helper package helper
// web helpers to either set Cross Origin Request Security or block all requests without a user
import ( import (
"context" "context"
"net/http" "net/http"
@@ -22,7 +24,7 @@ func SetCors(w http.ResponseWriter) {
func RequiresLogin(session *scs.SessionManager, w http.ResponseWriter, r *http.Request) { func RequiresLogin(session *scs.SessionManager, w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(), "session", session)) r = r.WithContext(context.WithValue(r.Context(), "session", session))
if GetEnv("GO_ENV", "production") == "debug" { if IsDebug() {
return return
} }
if session.Exists(r.Context(), "user") { if session.Exists(r.Context(), "user") {

View File

@@ -1,5 +1,7 @@
package main package main
// this is the main file where the webserver is configured and all endpoints are added with their routes
import ( import (
"arbeitszeitmessung/endpoints" "arbeitszeitmessung/endpoints"
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
@@ -36,7 +38,7 @@ func main() {
if err != nil { if err != nil {
slog.Info("No .env file found in directory!") slog.Info("No .env file found in directory!")
} }
if helper.GetEnv("GO_ENV", "production") == "debug" { if helper.IsDebug() {
logLevel.Set(slog.LevelDebug) logLevel.Set(slog.LevelDebug)
envs := os.Environ() envs := os.Environ()
slog.Debug("Debug mode enabled", "Environment Variables", envs) slog.Debug("Debug mode enabled", "Environment Variables", envs)
@@ -50,11 +52,13 @@ func main() {
defer models.DB.(*sql.DB).Close() defer models.DB.(*sql.DB).Close()
if helper.GetEnv("GO_ENV", "production") != "debug" {
err = Migrate() err = Migrate()
if err != nil { if err != nil {
slog.Error("Failed to migrate the database to newest version", "Error", err) slog.Error("Failed to migrate the database to newest version", "Error", err)
return return
} }
}
fs := http.FileServer(http.Dir("./static")) fs := http.FileServer(http.Dir("./static"))
endpoints.CreateSessionManager(24 * time.Hour) endpoints.CreateSessionManager(24 * time.Hour)
@@ -63,17 +67,15 @@ func main() {
// handles the different http routes // handles the different http routes
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("/auto/logout", endpoints.LogoutHandler) server.HandleFunc("/auto/logout", endpoints.LogoutHandler)
server.HandleFunc("/auto/kurzarbeit", endpoints.KurzarbeitFillHandler) server.HandleFunc("/auto/kurzarbeit", endpoints.KurzarbeitFillHandler)
server.HandleFunc("/auto/feiertage", endpoints.FeiertagsHandler) server.HandleFunc("/auto/feiertage", endpoints.FeiertagsHandler)
server.HandleFunc("/user/{action}", endpoints.UserHandler) server.HandleFunc("/user/{action}", endpoints.UserHandler)
// server.HandleFunc("/user/login", endpoints.LoginHandler) server.HandleFunc("/team/report", endpoints.ReportHandler)
// server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) server.HandleFunc("/team/presence", endpoints.PresenceHandler)
server.HandleFunc("/team", endpoints.TeamHandler) server.Handle("/pdf", paramsMiddleware(endpoints.PDFFormHandler))
server.HandleFunc("/presence", endpoints.TeamPresenceHandler)
server.Handle("/pdf", ParamsMiddleware(endpoints.PDFFormHandler))
server.HandleFunc("/pdf/generate", endpoints.PDFCreateController) server.HandleFunc("/pdf/generate", endpoints.PDFCreateController)
server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect)) server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect))
server.Handle("/static/", http.StripPrefix("/static/", fs)) server.Handle("/static/", http.StripPrefix("/static/", fs))
@@ -87,7 +89,7 @@ func main() {
slog.Error("Error starting Server", "Error", http.ListenAndServe(":8080", serverSessionMiddleware)) slog.Error("Error starting Server", "Error", http.ListenAndServe(":8080", serverSessionMiddleware))
} }
func ParamsMiddleware(next http.HandlerFunc) http.Handler { func paramsMiddleware(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query() queryParams := r.URL.Query()
ctx := context.WithValue(r.Context(), "urlParams", queryParams) ctx := context.WithValue(r.Context(), "urlParams", queryParams)

View File

@@ -1,9 +1,22 @@
// the models package contains the datamodels for the whole webserver
// most of them are also in the DB
//
// in the future it would be good to change it to create a repository structure,
// so that there would be a second pacakge handling db requests
package models package models
// this file has all functions and types to handle absences
// the absence type implements the iWorkDay interface so that
// it can be used as one workday
//
// the absence data is based on the entries in the "abwesenheit" database table
import ( import (
"database/sql"
"encoding/json" "encoding/json"
"errors"
"log" "log"
"log/slog"
"time" "time"
) )
@@ -27,25 +40,6 @@ func (a *Absence) IsEmpty() bool {
return false return false
} }
func NewAbsence(card_uid string, abwesenheit_typ int, datum time.Time) (Absence, error) {
if abwesenheit_typ < 0 {
return Absence{
CardUID: card_uid,
AbwesenheitTyp: AbsenceType{0, "Custom absence", 100},
DateFrom: datum,
}, nil
}
_absenceType, ok := GetAbsenceTypesCached()[int8(abwesenheit_typ)]
if !ok {
return Absence{}, errors.New("Invalid absencetype")
}
return Absence{
CardUID: card_uid,
AbwesenheitTyp: _absenceType,
DateFrom: datum,
}, nil
}
func (a *Absence) Date() time.Time { func (a *Absence) Date() time.Time {
return a.Day.Truncate(24 * time.Hour) return a.Day.Truncate(24 * time.Hour)
} }
@@ -69,7 +63,7 @@ func (a *Absence) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool)
return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100) return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100)
case WorktimeBaseWeek: case WorktimeBaseWeek:
if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit { if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit {
return u.ArbeitszeitProTagFrac(0.2) return u.ArbeitszeitProWocheFrac(0.2)
} else if a.AbwesenheitTyp.WorkTime <= 0 { } else if a.AbwesenheitTyp.WorkTime <= 0 {
return 0 return 0
} }
@@ -303,3 +297,24 @@ func (a *Absence) Delete() error {
_, err = qStr.Exec(a.CounterId) _, err = qStr.Exec(a.CounterId)
return err 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

@@ -1,5 +1,8 @@
package models_test package models_test
// all the files with *_test.go are used to test the functions inside the respective file
// the tests are partially complete
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"

View File

@@ -1,5 +1,11 @@
package models 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 ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/logs" "arbeitszeitmessung/helper/logs"
@@ -30,6 +36,7 @@ type Booking struct {
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
CounterId int `json:"counter_id"` CounterId int `json:"counter_id"`
BookingType BookingType `json:"anwesenheit_typ"` BookingType BookingType `json:"anwesenheit_typ"`
Valid bool `json:"valid"`
} }
type IDatabase interface { type IDatabase interface {
@@ -39,7 +46,7 @@ type IDatabase interface {
var DB IDatabase var DB IDatabase
func (b *Booking) New(cardUid string, gereatId int16, checkInOut int16, typeId int8) Booking { func (b *Booking) NewBooking(cardUid string, gereatId int16, checkInOut int16, typeId int8) Booking {
bookingType, err := GetBookingTypeById(typeId) bookingType, err := GetBookingTypeById(typeId)
if err != nil { if err != nil {
log.Printf("Cannot get booking type %d, from database!", typeId) log.Printf("Cannot get booking type %d, from database!", typeId)
@@ -88,27 +95,6 @@ func (b *Booking) Verify() bool {
return true 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 { func (b *Booking) Insert() error {
if !checkLastBooking(*b) { if !checkLastBooking(*b) {
return SameBookingError{} return SameBookingError{}
@@ -202,7 +188,7 @@ func (b Booking) Save() {
} }
func (b *Booking) GetBookingType() string { func (b *Booking) GetBookingType() string {
debug := (helper.GetEnv("GO_ENV", "production") == "debug") debug := helper.IsDebug()
switch b.CheckInOut { switch b.CheckInOut {
case 1: //manuelle Änderung case 1: //manuelle Änderung
return "kommen" return "kommen"
@@ -246,12 +232,13 @@ func (b *Booking) Update(nb Booking) {
func checkLastBooking(b Booking) bool { func checkLastBooking(b Booking) bool {
var check_in_out int var check_in_out int
slog.Info("Checking with timestamp:", "timestamp", b.Timestamp.String()) 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"::DATE <= $2::DATE ORDER BY "timestamp" DESC LIMIT 1;`)) stmt, err := DB.Prepare((`SELECT check_in_out FROM "anwesenheit" WHERE "card_uid" = $1 AND "timestamp" <= $2 ORDER BY "timestamp" DESC LIMIT 1;`))
if err != nil { if err != nil {
log.Fatalf("Error preparing query: %v", err) log.Fatalf("Error preparing query: %v", err)
return false return false
} }
err = stmt.QueryRow(b.CardUID, b.Timestamp).Scan(&check_in_out) err = stmt.QueryRow(b.CardUID, b.Timestamp).Scan(&check_in_out)
slog.Info("Checking last bookings check_in_out", "Check", check_in_out)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return true return true
} }

View File

@@ -1,5 +1,12 @@
package models package models
// this file contains all functions for the compound day class
// the compound can merge all kinds of daytypes together, as long as they
// implement the IWorkDay interface. This is used to have an absence + bookings
// in one day
//
// the compound day is a meta type, which means its not in the database
import ( import (
"log/slog" "log/slog"
"time" "time"
@@ -10,6 +17,17 @@ type CompoundDay struct {
DayParts []IWorkDay 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 { func NewCompondDay(date time.Time, dayParts ...IWorkDay) *CompoundDay {
return &CompoundDay{Day: date, DayParts: dayParts} return &CompoundDay{Day: date, DayParts: dayParts}
} }

View File

@@ -1,5 +1,8 @@
package models package models
// this file is only used as cache for the diffenrent absence and booking types,
// in the future this should be two chaching variables each in their own place
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
) )

View File

@@ -1,5 +1,10 @@
package models package models
// this file desribes the IWorkDay interface which is used, to combine the diffent kinds
// of day types (absence, holidy, workday) and make them more compatimble with the rest
//
// the IWorkDay as an interface is not in the database
import ( import (
"log/slog" "log/slog"
"time" "time"
@@ -18,6 +23,7 @@ type IWorkDay interface {
GetTimes(User, WorktimeBase, bool) (work, pause, overtime time.Duration) GetTimes(User, WorktimeBase, bool) (work, pause, overtime time.Duration)
GetOvertime(User, WorktimeBase, bool) time.Duration GetOvertime(User, WorktimeBase, bool) time.Duration
IsEmpty() bool IsEmpty() bool
IsSubmittedAndAccepted() bool
} }
type DayType int type DayType int
@@ -49,7 +55,9 @@ func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay
} }
for _, absentDay := range absences { for _, absentDay := range absences {
if weekDay := absentDay.Date().Weekday(); weekDay == time.Saturday || weekDay == time.Sunday {
continue
}
// Check if there is already a day // Check if there is already a day
existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)] existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)]
switch { switch {

View File

@@ -1,5 +1,10 @@
package models package models
// the public holiday is used the describe all holidays
// the publicholiday implements the IWorkDay interface
//
// the PublicHoliday data is based on the entries in the "s_feiertage" database table
import ( import (
"time" "time"
@@ -14,6 +19,11 @@ type PublicHoliday struct {
worktime int8 worktime int8
} }
// IsSubmittedAndAccepted implements IWorkDay.
func (p *PublicHoliday) IsSubmittedAndAccepted() bool {
return true
}
// IsEmpty implements [IWorkDay]. // IsEmpty implements [IWorkDay].
func (p *PublicHoliday) IsEmpty() bool { func (p *PublicHoliday) IsEmpty() bool {
return false return false

View File

@@ -1,5 +1,11 @@
package models package models
// the user type represents the user of the software
// it has functions to request the bookings and other data based
// on either the "PersonalNummer" or "CardUID"
//
// users and their passwords are stored in the "s_personal_daten" and "user_pass" tables
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"context" "context"
@@ -27,7 +33,7 @@ type User struct {
func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) { func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) {
var user User var user User
var err error var err error
if helper.GetEnv("GO_ENV", "production") == "debug" { if helper.IsDebug() {
user, err = GetUserByPersonalNr(123) user, err = GetUserByPersonalNr(123)
} else { } else {
if !Session.Exists(ctx, "user") { if !Session.Exists(ctx, "user") {
@@ -44,15 +50,15 @@ func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User,
} }
// Returns the actual overtime for this moment // Returns the actual overtime for this moment
func (u *User) GetReportedOvertime() (time.Duration, error) { func (u *User) GetReportedOvertime(startDate time.Time) (time.Duration, error) {
var overtime time.Duration var overtime time.Duration
qStr, err := DB.Prepare("SELECT COALESCE(SUM(EXTRACT(EPOCH FROM ueberstunden) * 1000000000)::BIGINT, 0) AS total_ueberstunden_ns FROM wochen_report WHERE personal_nummer = $1;") qStr, err := DB.Prepare("SELECT COALESCE(SUM(EXTRACT(EPOCH FROM ueberstunden) * 1000000000)::BIGINT, 0) AS total_ueberstunden_ns FROM wochen_report WHERE personal_nummer = $1 AND woche_start::DATE <= $2::DATE;")
if err != nil { if err != nil {
return 0, err return 0, err
} }
defer qStr.Close() defer qStr.Close()
err = qStr.QueryRow(u.PersonalNummer).Scan(&overtime) err = qStr.QueryRow(u.PersonalNummer, startDate).Scan(&overtime)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -149,7 +155,7 @@ func (u *User) CheckAnwesenheit() bool {
// Creates a new booking for the user -> check_in_out will be 254 for automatic check out // Creates a new booking for the user -> check_in_out will be 254 for automatic check out
func (u *User) CheckOut() error { func (u *User) CheckOut() error {
booking := (*Booking).New(nil, u.CardUID, 0, 254, 1) booking := (*Booking).NewBooking(nil, u.CardUID, 0, 254, 1)
err := booking.Insert() err := booking.Insert()
if err != nil { if err != nil {
fmt.Printf("Error inserting booking %v -> %v\n", booking, err) fmt.Printf("Error inserting booking %v -> %v\n", booking, err)
@@ -282,23 +288,46 @@ func (u *User) GetNextWeek() WorkWeek {
} }
func parseUser(rows *sql.Rows) (User, error) {
var user User
if err := rows.Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag); err != nil {
log.Println("Error scanning row!", err)
return user, err
}
return user, nil
}
// returns the start of the week, the last submission was made, submission == first booking or last send wochen_report to team leader // returns the start of the week, the last submission was made, submission == first booking or last send wochen_report to team leader
func (u *User) GetLastWorkWeekSubmission() time.Time { func (u *User) GetLastWorkWeekSubmission() time.Time {
var lastSub time.Time var lastSub time.Time
qStr, err := DB.Prepare(` qStr, err := DB.Prepare(`
SELECT COALESCE( SELECT new_week
(SELECT woche_start + INTERVAL '1 week' FROM wochen_report WHERE personal_nummer = $1 ORDER BY woche_start DESC LIMIT 1), FROM (
(SELECT timestamp FROM anwesenheit WHERE card_uid = $2 ORDER BY timestamp LIMIT 1) -- Highest priority
) AS letzte_buchung; 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 { if err != nil {
slog.Debug("Error preparing query statement.", "error", err) slog.Debug("Error preparing query statement.", "error", err)

View File

@@ -1,13 +1,21 @@
package models package models
// the workday combines all bookings of a day into a single type and is the third
// type of workday which implements the IWorkDay interface
//
// this is a meta type and not present in the db
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"log/slog" "log/slog"
"sort" "sort"
"time" "time"
"github.com/lib/pq"
) )
type WorkDay struct { type WorkDay struct {
@@ -41,11 +49,10 @@ func (d *WorkDay) GetWorktimeAbsence() Absence {
// Gets the time as is in the db (with corrected pause times) // Gets the time as is in the db (with corrected pause times)
func (d *WorkDay) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { func (d *WorkDay) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
if includeKurzarbeit && d.IsKurzArbeit() && len(d.Bookings) > 0 { if includeKurzarbeit && d.IsKurzArbeit() { //&& len(d.Bookings) > 0
return d.kurzArbeitAbsence.GetWorktime(u, base, true) return d.kurzArbeitAbsence.GetWorktime(u, base, true)
} }
work, pause := calcWorkPause(d.Bookings) work, _ := correctWorkPause(getWorkPause(d))
work, pause = correctWorkPause(work, pause)
if (d.worktimeAbsece != Absence{}) { if (d.worktimeAbsece != Absence{}) {
work += d.worktimeAbsece.GetWorktime(u, base, false) work += d.worktimeAbsece.GetWorktime(u, base, false)
} }
@@ -54,7 +61,7 @@ func (d *WorkDay) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool)
// Gets the corrected pause times based on db entries // Gets the corrected pause times based on db entries
func (d *WorkDay) GetPausetime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { func (d *WorkDay) GetPausetime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
work, pause := calcWorkPause(d.Bookings) work, pause := getWorkPause(d)
work, pause = correctWorkPause(work, pause) work, pause = correctWorkPause(work, pause)
return pause.Round(time.Minute) return pause.Round(time.Minute)
} }
@@ -76,6 +83,15 @@ func (d *WorkDay) GetTimes(u User, base WorktimeBase, includeKurzarbeit bool) (w
return d.GetWorktime(u, base, includeKurzarbeit), d.GetPausetime(u, base, includeKurzarbeit), d.GetOvertime(u, base, includeKurzarbeit) return d.GetWorktime(u, base, includeKurzarbeit), d.GetPausetime(u, base, includeKurzarbeit), d.GetOvertime(u, base, includeKurzarbeit)
} }
func getWorkPause(d *WorkDay) (work, pause time.Duration) {
//if today calc, else take from db
if helper.IsSameDate(d.Date(), time.Now()) {
return calcWorkPause(d.Bookings)
} else {
return d.workTime, d.pauseTime
}
}
func calcWorkPause(bookings []Booking) (work, pause time.Duration) { func calcWorkPause(bookings []Booking) (work, pause time.Duration) {
var lastBooking Booking var lastBooking Booking
for _, b := range bookings { for _, b := range bookings {
@@ -135,12 +151,21 @@ func (d *WorkDay) Type() DayType {
return DayTypeWorkday return DayTypeWorkday
} }
func (d *WorkDay) GenerateKurzArbeitBookings(u User) (time.Time, time.Time) { func (d *WorkDay) GenerateKurzArbeitBookings(u User, weekBase WorktimeBase) (time.Time, time.Time) {
var timeFrom, timeTo time.Time var timeFrom, timeTo time.Time
if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() { if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() {
return timeFrom, timeTo return timeFrom, timeTo
} }
if d.IsEmpty() {
switch weekBase {
case WorktimeBaseDay:
return d.Day.Add(time.Hour * 8), d.Day.Add(time.Hour * 8).Add(u.ArbeitszeitProTag())
case WorktimeBaseWeek:
return d.Day.Add(time.Hour * 8), d.Day.Add(time.Hour * 8).Add(u.ArbeitszeitProWocheFrac(0.2))
}
}
timeFrom = d.Bookings[len(d.Bookings)-1].Timestamp.Add(time.Minute) timeFrom = d.Bookings[len(d.Bookings)-1].Timestamp.Add(time.Minute)
timeTo = timeFrom.Add(u.ArbeitszeitProTag() - d.GetWorktime(u, WorktimeBaseDay, false)) timeTo = timeFrom.Add(u.ArbeitszeitProTag() - d.GetWorktime(u, WorktimeBaseDay, false))
slog.Debug("Added duration as Kurzarbeit", "date", d.Date().String(), "duration", timeTo.Sub(timeFrom).String()) slog.Debug("Added duration as Kurzarbeit", "date", d.Date().String(), "duration", timeTo.Sub(timeFrom).String())
@@ -153,7 +178,7 @@ func (d *WorkDay) GetKurzArbeit() *Absence {
} }
func (d *WorkDay) ToString() string { func (d *WorkDay) ToString() string {
return fmt.Sprintf("WorkDay: %s with %d bookings and worktime: %s", d.Date().Format(time.DateOnly), len(d.Bookings), helper.FormatDuration(d.workTime)) return fmt.Sprintf("WorkDay: %s with %d bookings and worktime: %s. Is KurzArbeit %v", d.Date().Format(time.DateOnly), len(d.Bookings), helper.FormatDuration(d.workTime), d.IsKurzArbeit())
} }
func (d *WorkDay) IsWorkDay() bool { func (d *WorkDay) IsWorkDay() bool {
@@ -173,11 +198,16 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
var workSec, pauseSec float64 var workSec, pauseSec float64
qStr, err := DB.Prepare(` qStr, err := DB.Prepare(`
WITH all_days AS ( WITH
SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date), all_days AS (
normalized_bookings AS ( SELECT
SELECT * generate_series(
FROM ( $2 ::DATE,
$3 ::DATE - INTERVAL '1 day',
INTERVAL '1 day'
)::DATE AS work_date
),
all_bookings AS (
SELECT SELECT
a.card_uid, a.card_uid,
a.timestamp, a.timestamp,
@@ -187,82 +217,128 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
a.anwesenheit_typ, a.anwesenheit_typ,
sat.anwesenheit_name AS anwesenheit_typ_name, sat.anwesenheit_name AS anwesenheit_typ_name,
LAG(a.check_in_out) OVER ( LAG(a.check_in_out) OVER (
PARTITION BY a.card_uid, a.timestamp::DATE PARTITION BY
ORDER BY a.timestamp a.card_uid,
) AS prev_check a.timestamp::DATE
FROM anwesenheit a ORDER BY
LEFT JOIN s_anwesenheit_typen sat a.timestamp
ON a.anwesenheit_typ = sat.anwesenheit_id ) AS prev_check,
WHERE a.card_uid = $1 LAG(a.timestamp) OVER (
AND a.timestamp::DATE >= $2 PARTITION BY
AND a.timestamp::DATE <= $3 a.card_uid,
) t a.timestamp::DATE
WHERE prev_check IS NULL OR prev_check <> check_in_out ORDER BY
), a.timestamp
ordered_bookings AS (
SELECT
*,
LAG(timestamp) OVER (
PARTITION BY card_uid, work_date
ORDER BY timestamp
) AS prev_timestamp ) AS prev_timestamp
FROM normalized_bookings FROM
anwesenheit a
LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id
WHERE
a.card_uid = $1
AND a.timestamp::DATE >= $2::DATE
AND a.timestamp::DATE <= $3::DATE
),
normalized_bookings AS (
SELECT
*
FROM
all_bookings
WHERE
prev_check IS NULL
OR prev_check <> check_in_out
) )
SELECT SELECT
d.work_date, d.work_date,
COALESCE(MIN(b.timestamp), NOW()) AS time_from, COALESCE(MIN(b.timestamp), NOW()) AS time_from,
COALESCE(MAX(b.timestamp), NOW()) AS time_to, COALESCE(MAX(b.timestamp), NOW()) AS time_to,
COALESCE( EXTRACT(
EXTRACT(EPOCH FROM SUM( EPOCH
FROM
SUM(
CASE CASE
WHEN b.prev_check IN (1, 3) AND b.check_in_out IN (2, 4, 254) WHEN b.prev_check IN (1, 3)
THEN b.timestamp - b.prev_timestamp AND b.check_in_out IN (2, 4, 254) THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0' ELSE INTERVAL '0'
END END
)), 0
) AS total_work_seconds,
COALESCE(
EXTRACT(EPOCH FROM SUM(
CASE
WHEN b.prev_check IN (2, 4, 254) AND b.check_in_out IN (1, 3)
THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0'
END
)), 0
) AS total_pause_seconds,
COALESCE(jsonb_agg(jsonb_build_object(
'check_in_out', b.check_in_out,
'timestamp', b.timestamp,
'counter_id', b.counter_id,
'anwesenheit_typ', b.anwesenheit_typ,
'anwesenheit_typ', jsonb_build_object(
'anwesenheit_id', b.anwesenheit_typ,
'anwesenheit_name', b.anwesenheit_typ_name
) )
) ORDER BY b.timestamp), '[]'::jsonb) AS bookings ) AS total_work_seconds,
FROM all_days d EXTRACT(
LEFT JOIN ordered_bookings b ON d.work_date = b.work_date EPOCH
GROUP BY d.work_date FROM
ORDER BY d.work_date ASC;`) SUM(
CASE
WHEN b.prev_check IN (2, 4, 254)
AND b.check_in_out IN (1, 3) THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0'
END
)
) AS total_pause_seconds,
jsonb_agg(
jsonb_build_object(
'check_in_out',
b.check_in_out,
'valid',
coalesce(b.check_in_out != b.prev_check, true),
'timestamp',
b.timestamp,
'counter_id',
b.counter_id,
'anwesenheit_typ',
jsonb_build_object(
'anwesenheit_id',
b.anwesenheit_typ,
'anwesenheit_name',
b.anwesenheit_typ_name
)
)
ORDER BY
b.timestamp
) FILTER (
WHERE
b.card_uid IS NOT NULL
) AS bookings
FROM
all_days d
LEFT JOIN all_bookings b ON b.work_date = d.work_date
GROUP BY
d.work_date;
`)
// qStr, err := DB.Prepare(` // qStr, err := DB.Prepare(`
// WITH all_days AS ( // WITH all_days AS (
// SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date), // SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date),
// ordered_bookings AS ( // normalized_bookings AS (
// SELECT *
// FROM (
// SELECT // SELECT
// a.timestamp::DATE AS work_date, // a.card_uid,
// a.timestamp, // a.timestamp,
// a.timestamp::DATE AS work_date,
// a.check_in_out, // a.check_in_out,
// a.counter_id, // a.counter_id,
// a.anwesenheit_typ, // a.anwesenheit_typ,
// sat.anwesenheit_name AS anwesenheit_typ_name, // sat.anwesenheit_name AS anwesenheit_typ_name,
// LAG(a.timestamp) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_timestamp, // LAG(a.check_in_out) OVER (
// LAG(a.check_in_out) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_check // PARTITION BY a.card_uid, a.timestamp::DATE
// ORDER BY a.timestamp
// ) AS prev_check
// FROM anwesenheit a // FROM anwesenheit a
// LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id // LEFT JOIN s_anwesenheit_typen sat
// ON a.anwesenheit_typ = sat.anwesenheit_id
// WHERE a.card_uid = $1 // WHERE a.card_uid = $1
// AND a.timestamp::DATE >= $2 // AND a.timestamp::DATE >= $2
// AND a.timestamp::DATE <= $3 // AND a.timestamp::DATE <= $3
// ) t
// WHERE prev_check IS NULL OR prev_check <> check_in_out
// ),
// ordered_bookings AS (
// SELECT
// *,
// LAG(timestamp) OVER (
// PARTITION BY card_uid, work_date
// ORDER BY timestamp
// ) AS prev_timestamp
// FROM normalized_bookings
// ) // )
// SELECT // SELECT
// d.work_date, // d.work_date,
@@ -317,26 +393,29 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
var workDay WorkDay var workDay WorkDay
var bookings []byte var bookings []byte
if err := rows.Scan(&workDay.Day, &workDay.TimeFrom, &workDay.TimeTo, &workSec, &pauseSec, &bookings); err != nil { if err := rows.Scan(&workDay.Day, &workDay.TimeFrom, &workDay.TimeTo, &workSec, &pauseSec, &bookings); err != nil {
log.Println("Error scanning row!", err) slog.Error("Error scanning row!", "Error", err)
return workDays return workDays
} }
workDay.workTime = time.Duration(workSec * float64(time.Second)) workDay.workTime = time.Duration(workSec * float64(time.Second))
workDay.pauseTime = time.Duration(pauseSec * float64(time.Second)) workDay.pauseTime = time.Duration(pauseSec * float64(time.Second))
if bookings != nil {
err = json.Unmarshal(bookings, &workDay.Bookings) err = json.Unmarshal(bookings, &workDay.Bookings)
if err != nil { if err != nil {
log.Println("Error parsing bookings JSON!", err) slog.Error("Error parsing bookings JSON!", "Error", err, "Json", bookings)
return nil return nil
} }
// better empty day handling
if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 {
workDay.Bookings = []Booking{}
} }
// better empty day handling
// if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 {
// workDay.Bookings = []Booking{}
// }
if len(workDay.Bookings) >= 1 || !helper.IsWeekend(workDay.Date()) { if len(workDay.Bookings) >= 1 || !helper.IsWeekend(workDay.Date()) {
workDays = append(workDays, workDay) workDays = append(workDays, workDay)
} }
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
log.Println("Error in workday rows!", err) slog.Error("Error in workday rows!", "Error", err)
return workDays return workDays
} }
return workDays return workDays
@@ -358,3 +437,38 @@ func (d *WorkDay) GetDayProgress(u User) int8 {
progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100 progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100
return int8(progress) 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

@@ -1,6 +1,12 @@
package models package models
// the WorkWeek describes the weekly reports used the check on worktimes and
// calculate the running overtime
//
// this type is based on the "wochen_report" table
import ( import (
"arbeitszeitmessung/helper"
"database/sql" "database/sql"
"errors" "errors"
"log" "log"
@@ -19,25 +25,34 @@ type WorkWeek struct {
Days []IWorkDay Days []IWorkDay
User User User User
WeekStart time.Time WeekStart time.Time
weekEnd time.Time
Worktime time.Duration Worktime time.Duration
WorktimeVirtual time.Duration WorktimeVirtual time.Duration
Overtime time.Duration Overtime time.Duration
Status WeekStatus Status WeekStatus
WeekBase WorktimeBase
Kurzarbeit time.Duration
} }
type WeekStatus int8 type WeekStatus int8
const ( const (
WeekStatusNone WeekStatus = iota WeekStatusNone WeekStatus = iota
WeekStatusCorrected
WeekStatusSent WeekStatusSent
WeekStatusAccepted WeekStatusAccepted
WeekStatusDifferences WeekStatusDifferences
) )
func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek { func NewWorkWeekSimple(user User, tsMonday time.Time, populate bool) WorkWeek {
return NewWorkWeek(user, tsMonday, tsMonday.Add(6*24*time.Hour), populate)
}
func NewWorkWeek(user User, tsStart, tsEnd time.Time, populate bool) WorkWeek {
var week WorkWeek = WorkWeek{ var week WorkWeek = WorkWeek{
User: user, User: user,
WeekStart: tsMonday, WeekStart: tsStart,
weekEnd: tsEnd,
Status: WeekStatusNone, Status: WeekStatusNone,
} }
if populate { if populate {
@@ -47,13 +62,25 @@ func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek {
} }
func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Duration) { func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Duration) {
slog.Debug("Populating Workweek for user", "user", w.User)
slog.Debug("Got Days with overtime and worktime", slog.String("worktime", worktime.String()), slog.String("overtime", overtime.String())) slog.Debug("Got Days with overtime and worktime", slog.String("worktime", worktime.String()), slog.String("overtime", overtime.String()))
w.Days = GetDays(w.User, w.WeekStart, w.WeekStart.Add(6*24*time.Hour), false) w.Days = GetDays(w.User, w.WeekStart, w.weekEnd, false)
slog.Debug("Populating Workweek for user", "user", w.User.Name, "Days", lenWorkDays(w.Days), "Start", w.WeekStart, "End", w.weekEnd, "workdays", helper.GetWorkingDays(w.WeekStart, w.weekEnd))
if lenWorkDays(w.Days) == helper.GetWorkingDays(w.WeekStart, w.weekEnd) {
w.WeekBase = WorktimeBaseWeek
} else {
w.WeekBase = WorktimeBaseDay
}
for _, day := range w.Days { for _, day := range w.Days {
w.Worktime += day.GetWorktime(w.User, WorktimeBaseDay, false) dWorkTime := day.GetWorktime(w.User, w.WeekBase, false)
w.WorktimeVirtual += day.GetWorktime(w.User, WorktimeBaseDay, true) dWorkTimeVirtual := day.GetWorktime(w.User, w.WeekBase, true)
if dWorkTime < dWorkTimeVirtual {
w.Kurzarbeit += dWorkTimeVirtual - dWorkTime
}
w.Worktime += dWorkTime
w.WorktimeVirtual += dWorkTimeVirtual
slog.Debug("Calculated Worktime", "Day", day.ToString(), "worktime", w.Worktime.String())
} }
slog.Debug("Got worktime for user", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorktimeVirtual.String()) slog.Debug("Got worktime for user", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorktimeVirtual.String())
@@ -73,6 +100,16 @@ func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Durati
} }
} }
func lenWorkDays(workDays []IWorkDay) int {
var lenght int
for _, day := range workDays {
if !day.IsEmpty() || day.IsKurzArbeit() {
lenght += 1
}
}
return lenght
}
func (w *WorkWeek) CheckStatus() WeekStatus { func (w *WorkWeek) CheckStatus() WeekStatus {
if w.Status != WeekStatusNone { if w.Status != WeekStatusNone {
return w.Status return w.Status
@@ -81,25 +118,31 @@ func (w *WorkWeek) CheckStatus() WeekStatus {
log.Println("Cannot access Database!") log.Println("Cannot access Database!")
return w.Status 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 { if err != nil {
log.Println("Error preparing SQL statement", err) log.Println("Error preparing SQL statement", err)
return w.Status return w.Status
} }
defer qStr.Close() defer qStr.Close()
var beastatigt bool var beastatigt sql.NullBool
err = qStr.QueryRow(w.WeekStart, w.User.PersonalNummer).Scan(&beastatigt) err = qStr.QueryRow(w.WeekStart, w.User.PersonalNummer).Scan(&beastatigt, &w.Id)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return w.Status return w.Status
} }
slog.Info("Bestätigt query res", "Best", beastatigt, "week", w.Id)
if err != nil { if err != nil {
log.Println("Error querying database", err) log.Println("Error querying database", err)
return w.Status return w.Status
} }
if beastatigt { switch {
case beastatigt.Bool:
w.Status = WeekStatusAccepted w.Status = WeekStatusAccepted
} else { case beastatigt.Valid:
w.Status = WeekStatusSent w.Status = WeekStatusSent
default:
w.Status = WeekStatusCorrected
} }
return w.Status return w.Status
} }
@@ -201,23 +244,33 @@ func (w *WorkWeek) SendWeek() error {
return ErrRunningWeek return ErrRunningWeek
} }
if w.CheckStatus() != WeekStatusNone { switch w.CheckStatus() {
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;`) case WeekStatusNone:
if err != nil {
slog.Warn("Error preparing SQL statement", "error", err)
return err
}
} else {
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);`) 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 { if err != nil {
slog.Warn("Error preparing SQL statement", "error", err) slog.Warn("Error preparing SQL statement", "error", err)
return 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)) _, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, int64(w.Worktime), int64(w.Overtime), pq.Array(anwBookings), pq.Array(awBookings))
if err != nil { if err != nil {
log.Println("Error executing query!", err) slog.Error("Error executing query!", "error", err)
return err return err
} }
return nil return nil

View File

@@ -20,7 +20,7 @@ func TestNewWorkWeekNoPopulate(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
workWeek := models.NewWorkWeek(testUser, monday, false) workWeek := models.NewWorkWeekSimple(testUser, monday, false)
if workWeek.User != testUser || workWeek.WeekStart != monday { if workWeek.User != testUser || workWeek.WeekStart != monday {
t.Error("No populate workweek does not have right values!") t.Error("No populate workweek does not have right values!")

View File

@@ -78,14 +78,8 @@
border-style: var(--tw-border-style); border-style: var(--tw-border-style);
border-width: 1px; border-width: 1px;
border-color: var(--color-neutral-800); border-color: var(--color-neutral-800);
transition-property: transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
color, background-color, border-color, outline-color, transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
text-decoration-color, fill, stroke, --tw-gradient-from,
--tw-gradient-via, --tw-gradient-to;
transition-timing-function: var(
--tw-ease,
var(--default-transition-timing-function)
);
transition-duration: var(--tw-duration, var(--default-transition-duration)); transition-duration: var(--tw-duration, var(--default-transition-duration));
} }

View File

@@ -20,7 +20,6 @@
--color-neutral-300: oklch(87% 0 0); --color-neutral-300: oklch(87% 0 0);
--color-neutral-400: oklch(70.8% 0 0); --color-neutral-400: oklch(70.8% 0 0);
--color-neutral-500: oklch(55.6% 0 0); --color-neutral-500: oklch(55.6% 0 0);
--color-neutral-600: oklch(43.9% 0 0);
--color-neutral-700: oklch(37.1% 0 0); --color-neutral-700: oklch(37.1% 0 0);
--color-neutral-800: oklch(26.9% 0 0); --color-neutral-800: oklch(26.9% 0 0);
--color-black: #000; --color-black: #000;
@@ -30,8 +29,6 @@
--text-sm--line-height: calc(1.25 / 0.875); --text-sm--line-height: calc(1.25 / 0.875);
--text-xl: 1.25rem; --text-xl: 1.25rem;
--text-xl--line-height: calc(1.75 / 1.25); --text-xl--line-height: calc(1.75 / 1.25);
--text-2xl: 1.5rem;
--text-2xl--line-height: calc(2 / 1.5);
--font-weight-bold: 700; --font-weight-bold: 700;
--radius-md: 0.375rem; --radius-md: 0.375rem;
--default-transition-duration: 150ms; --default-transition-duration: 150ms;
@@ -205,45 +202,24 @@
.top-0 { .top-0 {
top: calc(var(--spacing) * 0); top: calc(var(--spacing) * 0);
} }
.top-1 {
top: calc(var(--spacing) * 1);
}
.top-1\/2 { .top-1\/2 {
top: calc(1/2 * 100%); top: calc(1/2 * 100%);
} }
.top-2 {
top: calc(var(--spacing) * 2);
}
.top-2\.5 { .top-2\.5 {
top: calc(var(--spacing) * 2.5); top: calc(var(--spacing) * 2.5);
} }
.top-25 {
top: calc(var(--spacing) * 25);
}
.top-26 {
top: calc(var(--spacing) * 26);
}
.top-\[0\.125rem\] { .top-\[0\.125rem\] {
top: 0.125rem; top: 0.125rem;
} }
.right-1 { .right-1 {
right: calc(var(--spacing) * 1); right: calc(var(--spacing) * 1);
} }
.right-2 {
right: calc(var(--spacing) * 2);
}
.right-2\.5 { .right-2\.5 {
right: calc(var(--spacing) * 2.5); right: calc(var(--spacing) * 2.5);
} }
.left-1 {
left: calc(var(--spacing) * 1);
}
.left-1\/2 { .left-1\/2 {
left: calc(1/2 * 100%); left: calc(1/2 * 100%);
} }
.z-10 {
z-index: 10;
}
.z-100 { .z-100 {
z-index: 100; z-index: 100;
} }
@@ -262,6 +238,9 @@
.-my-1 { .-my-1 {
margin-block: calc(var(--spacing) * -1); margin-block: calc(var(--spacing) * -1);
} }
.my-2 {
margin-block: calc(var(--spacing) * 2);
}
.mt-1 { .mt-1 {
margin-top: calc(var(--spacing) * 1); margin-top: calc(var(--spacing) * 1);
} }
@@ -329,6 +308,32 @@
mask-size: 100% 100%; mask-size: 100% 100%;
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M7.616 20q-.672 0-1.144-.472T6 18.385V6H5V5h4v-.77h6V5h4v1h-1v12.385q0 .69-.462 1.153T16.384 20zM17 6H7v12.385q0 .269.173.442t.443.173h8.769q.23 0 .423-.192t.192-.424zM9.808 17h1V8h-1zm3.384 0h1V8h-1zM7 6v13z'/%3E%3C/svg%3E"); --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M7.616 20q-.672 0-1.144-.472T6 18.385V6H5V5h4v-.77h6V5h4v1h-1v12.385q0 .69-.462 1.153T16.384 20zM17 6H7v12.385q0 .269.173.442t.443.173h8.769q.23 0 .423-.192t.192-.424zM9.808 17h1V8h-1zm3.384 0h1V8h-1zM7 6v13z'/%3E%3C/svg%3E");
} }
.icon-\[material-symbols-light--edit-calendar-rounded\] {
display: inline-block;
width: 1.25em;
height: 1.25em;
background-color: currentColor;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M5.616 21q-.691 0-1.153-.462T4 19.385V6.615q0-.69.463-1.152T5.616 5h1.769V3.308q0-.23.155-.384q.156-.155.386-.155t.383.155t.153.384V5h7.154V3.27q0-.213.143-.357q.144-.144.357-.144t.356.144t.144.356V5h1.769q.69 0 1.153.463T20 6.616v4.601q0 .213-.144.356t-.357.144t-.356-.144t-.143-.356v-.602H5v8.77q0 .23.192.423t.423.192h5.731q.213 0 .357.144t.143.357t-.143.356t-.357.143zm8.615-.808V19.12q0-.153.056-.296q.055-.144.186-.275l5.09-5.065q.149-.13.306-.19t.315-.062q.172 0 .338.064q.166.065.301.194l.925.944q.123.148.188.308q.064.159.064.319t-.052.322t-.2.31l-5.065 5.066q-.131.13-.275.186q-.143.056-.297.056h-1.073q-.343 0-.575-.232t-.232-.576m5.96-4.177l.925-.956l-.925-.944l-.95.95z'/%3E%3C/svg%3E");
}
.icon-\[material-symbols-light--lock\] {
display: inline-block;
width: 1.25em;
height: 1.25em;
background-color: currentColor;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M6.616 21q-.667 0-1.141-.475T5 19.386v-8.77q0-.666.475-1.14T6.615 9H8V7q0-1.671 1.165-2.835Q10.329 3 12 3t2.836 1.165T16 7v2h1.385q.666 0 1.14.475t.475 1.14v8.77q0 .666-.475 1.14t-1.14.475zM12 16.5q.633 0 1.066-.434q.434-.433.434-1.066t-.434-1.066T12 13.5t-1.066.434Q10.5 14.367 10.5 15t.434 1.066q.433.434 1.066.434M9 9h6V7q0-1.25-.875-2.125T12 4t-2.125.875T9 7z'/%3E%3C/svg%3E");
}
.icon-\[material-symbols-light--more-time\] { .icon-\[material-symbols-light--more-time\] {
display: inline-block; display: inline-block;
width: 1.25em; width: 1.25em;
@@ -371,6 +376,9 @@
.block { .block {
display: block; display: block;
} }
.contents {
display: contents;
}
.flex { .flex {
display: flex; display: flex;
} }
@@ -401,12 +409,13 @@
width: calc(var(--spacing) * 5); width: calc(var(--spacing) * 5);
height: calc(var(--spacing) * 5); height: calc(var(--spacing) * 5);
} }
.size-6 {
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
}
.h-2 { .h-2 {
height: calc(var(--spacing) * 2); height: calc(var(--spacing) * 2);
} }
.h-3 {
height: calc(var(--spacing) * 3);
}
.h-3\.5 { .h-3\.5 {
height: calc(var(--spacing) * 3.5); height: calc(var(--spacing) * 3.5);
} }
@@ -431,9 +440,6 @@
.w-2 { .w-2 {
width: calc(var(--spacing) * 2); width: calc(var(--spacing) * 2);
} }
.w-3 {
width: calc(var(--spacing) * 3);
}
.w-3\.5 { .w-3\.5 {
width: calc(var(--spacing) * 3.5); width: calc(var(--spacing) * 3.5);
} }
@@ -443,9 +449,6 @@
.w-5 { .w-5 {
width: calc(var(--spacing) * 5); width: calc(var(--spacing) * 5);
} }
.w-9 {
width: calc(var(--spacing) * 9);
}
.w-9\/10 { .w-9\/10 {
width: calc(9/10 * 100%); width: calc(9/10 * 100%);
} }
@@ -458,9 +461,6 @@
.w-full { .w-full {
width: 100%; width: 100%;
} }
.flex-shrink {
flex-shrink: 1;
}
.flex-shrink-0 { .flex-shrink-0 {
flex-shrink: 0; flex-shrink: 0;
} }
@@ -476,21 +476,10 @@
.basis-\[content\] { .basis-\[content\] {
flex-basis: content; flex-basis: content;
} }
.border-collapse {
border-collapse: collapse;
}
.-translate-x-1 {
--tw-translate-x: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-x-1\/2 { .-translate-x-1\/2 {
--tw-translate-x: calc(calc(1/2 * 100%) * -1); --tw-translate-x: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
} }
.-translate-y-1 {
--tw-translate-y: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-1\/2 { .-translate-y-1\/2 {
--tw-translate-y: calc(calc(1/2 * 100%) * -1); --tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -501,36 +490,18 @@
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.resize {
resize: both;
}
.scroll-m-2 { .scroll-m-2 {
scroll-margin: calc(var(--spacing) * 2); scroll-margin: calc(var(--spacing) * 2);
} }
.appearance-none { .appearance-none {
appearance: none; appearance: none;
} }
.break-after-page {
break-after: page;
}
.auto-rows-min {
grid-auto-rows: min-content;
}
.grid-cols-2 { .grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.grid-cols-5 { .grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
} }
.grid-cols-\[3fr_2fr_2fr_2fr_3fr_3fr_3fr\] {
grid-template-columns: 3fr 2fr 2fr 2fr 3fr 3fr 3fr;
}
.grid-cols-subgrid {
grid-template-columns: subgrid;
}
.grid-rows-6 {
grid-template-rows: repeat(6, minmax(0, 1fr));
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
@@ -581,11 +552,6 @@
border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
} }
} }
.divide-neutral-300 {
:where(& > :not(:last-child)) {
border-color: var(--color-neutral-300);
}
}
.justify-self-end { .justify-self-end {
justify-self: flex-end; justify-self: flex-end;
} }
@@ -612,18 +578,10 @@
border-style: var(--tw-border-style); border-style: var(--tw-border-style);
border-width: 0px; border-width: 0px;
} }
.border-r-0 {
border-right-style: var(--tw-border-style);
border-right-width: 0px;
}
.border-r-1 { .border-r-1 {
border-right-style: var(--tw-border-style); border-right-style: var(--tw-border-style);
border-right-width: 1px; border-right-width: 1px;
} }
.border-b-0 {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 0px;
}
.border-dashed { .border-dashed {
--tw-border-style: dashed; --tw-border-style: dashed;
border-style: dashed; border-style: dashed;
@@ -634,9 +592,6 @@
.border-neutral-500 { .border-neutral-500 {
border-color: var(--color-neutral-500); border-color: var(--color-neutral-500);
} }
.border-neutral-600 {
border-color: var(--color-neutral-600);
}
.border-slate-800 { .border-slate-800 {
border-color: var(--color-slate-800); border-color: var(--color-slate-800);
} }
@@ -661,18 +616,12 @@
.bg-red-600 { .bg-red-600 {
background-color: var(--color-red-600); background-color: var(--color-red-600);
} }
.mask-repeat {
mask-repeat: repeat;
}
.p-1 { .p-1 {
padding: calc(var(--spacing) * 1); padding: calc(var(--spacing) * 1);
} }
.p-2 { .p-2 {
padding: calc(var(--spacing) * 2); padding: calc(var(--spacing) * 2);
} }
.p-8 {
padding: calc(var(--spacing) * 8);
}
.px-3 { .px-3 {
padding-inline: calc(var(--spacing) * 3); padding-inline: calc(var(--spacing) * 3);
} }
@@ -685,10 +634,6 @@
.text-center { .text-center {
text-align: center; text-align: center;
} }
.text-2xl {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
.text-sm { .text-sm {
font-size: var(--text-sm); font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height)); line-height: var(--tw-leading, var(--text-sm--line-height));
@@ -710,9 +655,6 @@
.text-black { .text-black {
color: var(--color-black); color: var(--color-black);
} }
.text-neutral-300 {
color: var(--color-neutral-300);
}
.text-neutral-500 { .text-neutral-500 {
color: var(--color-neutral-500); color: var(--color-neutral-500);
} }
@@ -740,16 +682,9 @@
.uppercase { .uppercase {
text-transform: uppercase; text-transform: uppercase;
} }
.underline {
text-decoration-line: underline;
}
.opacity-0 { .opacity-0 {
opacity: 0%; opacity: 0%;
} }
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.filter { .filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
} }
@@ -776,18 +711,6 @@
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
} }
.\*\:text-center {
:is(& > *) {
text-align: center;
}
}
.\*\:not-print\:p-2 {
:is(& > *) {
@media not print {
padding: calc(var(--spacing) * 2);
}
}
}
.group-hover\:text-black { .group-hover\:text-black {
&:is(:where(.group):hover *) { &:is(:where(.group):hover *) {
@media (hover: hover) { @media (hover: hover) {
@@ -1195,11 +1118,6 @@
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
} }
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur { @property --tw-blur {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@@ -1272,7 +1190,6 @@
--tw-border-style: solid; --tw-border-style: solid;
--tw-divide-y-reverse: 0; --tw-divide-y-reverse: 0;
--tw-font-weight: initial; --tw-font-weight: initial;
--tw-outline-style: solid;
--tw-blur: initial; --tw-blur: initial;
--tw-brightness: initial; --tw-brightness: initial;
--tw-contrast: initial; --tw-contrast: initial;

View File

@@ -1,92 +0,0 @@
#let table-header(..headers) = {
table.header(
..headers.pos().map(h => strong(h))
)
}
#let abrechnung(meta, days) = {
set page(paper: "a4", margin: (x:1.5cm, y:2.25cm),
footer:[#grid(
columns: (3fr, .65fr),
align: left + horizon,
inset: .5em,
[#meta.EmployeeName -- #meta.TimeRange], grid.cell(rowspan: 2)[#image("static/logo.png")],
[Arbeitszeitrechnung maschinell erstellt am #meta.CurrentTimestamp],
)
])
set text(font: "Noto Sans", size:10pt, fill: luma(10%))
set table(
stroke: 0.5pt + luma(10%),
inset: .5em,
align: center + horizon,
)
show text: it => {
if it.text == "0min"{
text(oklch(70.8%, 0, 0deg))[#it]
}else if it.text.starts-with("-"){
text(red)[#it]
}else{
it
}
}
[= Abrechnung Arbeitszeit -- #meta.EmployeeName]
[Zeitraum: #meta.TimeRange]
table(
columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1.25fr),
fill: (x, y) =>
if y == 0 { oklch(87%, 0, 0deg) },
table-header(
[Datum], [Kommen], [Gehen], [Arbeitsart], [Stunden], [Pause], [Überstunden]
),
.. for day in days {
(
[#day.Date],
if day.DayParts.len() == 0{
table.cell(colspan: 3)[Keine Buchungen]
}else if not day.DayParts.first().IsWorkDay{
table.cell(colspan: 3)[#day.DayParts.first().WorkType]
}
else {
table.cell(colspan: 3, inset: 0em)[
#table(
columns: (1fr, 1fr, 1fr),
.. for Zeit in day.DayParts {
(
[#Zeit.BookingFrom],
[#Zeit.BookingTo],
[#Zeit.WorkType],
)
},
)
]
},
[#day.Worktime],
[#day.Pausetime],
[#day.Overtime],
)
if day.IsFriday {
( table.cell(colspan: 7, fill: oklch(87%, 0, 0deg))[Wochenende], ) // note the trailing comma
}
}
)
table(
columns: (3fr, 1fr),
align: right,
inset: (x: .25em, y:.75em),
stroke: none,
table.hline(start: 0, end: 2, stroke: stroke(dash:"dashed", thickness:.5pt)),
[Arbeitszeit :], table.cell(align: left)[#meta.WorkTime],
[Überstunden :], table.cell(align: left)[#meta.Overtime],
[Überstunden :],table.cell(align: left)[#meta.OvertimeTotal],
table.hline(start: 0, end: 2),
)
}

View File

@@ -0,0 +1,97 @@
// templates are used for the templ templates
// these will render the html page with data from the webserver
package templates
import (
"arbeitszeitmessung/models"
"fmt"
)
// this file includes the basic components, which are used in many other components and pages
templ headerComponent() {
// {{ user := ctx.Value("user").(models.User) }}
<div class="flex flex-row justify-between md:mx-[10%] py-2 items-center">
<a href="/time">Zeitverwaltung</a>
<a href="/team/report">Abrechnung</a>
if true {
<a href="/pdf">Monatsabrechnung</a>
<a href="/team/presence">Anwesenheit</a>
}
<a href="/user/settings">Einstellungen</a>
<button onclick="logoutUser()" type="button" class="cursor-pointer">Abmelden</button>
</div>
}
templ statusCheckMark(status models.WeekStatus, target models.WeekStatus) {
if status >= target {
<div class="icon-[material-symbols-light--check-circle-outline]"></div>
} else {
<div class="icon-[material-symbols-light--circle-outline]"></div>
}
}
templ lineComponent() {
<div class="flex flex-col w-2 py-2 items-center text-accent print:hidden">
<svg class="size-2" viewBox="0 0 24 24" fill="currentColor">
<polygon points="12,2 22,12 12,22 2,12"></polygon>
</svg>
<div class="w-[2px] bg-accent flex-grow -my-1"></div>
<svg class="size-2" viewBox="0 0 24 24" fill="currentColor">
<polygon points="12,2 22,12 12,22 2,12"></polygon>
</svg>
</div>
}
templ timeGaugeComponent(progress int8, today bool) {
{{
var bgColor string
switch {
case (0 > progress):
bgColor = "bg-red-600"
break
case (progress > 0 && progress < 95):
bgColor = "bg-orange-500"
break
case (95 <= progress && progress <= 105):
bgColor = "bg-accent"
break
case (progress > 105):
bgColor = "bg-purple-600"
break
default:
bgColor = "bg-neutral-400"
break
}
}}
if today {
<div class="flex-start flex w-2 h-full overflow-hidden rounded-full bg-neutral-300 print:hidden">
<div class={ "flex w-full items-center justify-center overflow-hidden rounded-full", bgColor } style={ fmt.Sprintf("height: %d%%", int(progress)) }></div>
</div>
} else {
<div class={ "w-2 h-full bg-accent rounded-md flex-shrink-0", bgColor }></div>
}
}
templ legendComponent() {
<div class="flex flex-row gap-4 md:mx-[10%] print:hidden">
<div class="flex flex-row items-center gap-2"><div class="rounded-full size-4 bg-red-600"></div><span>Fehler</span></div>
<div class="flex flex-row items-center gap-2"><div class="rounded-full size-4 bg-orange-500"></div><span>Arbeitszeit unter regulär</span></div>
<div class="flex flex-row items-center gap-2"><div class="rounded-full size-4 bg-accent"></div><span>Arbeitszeit vollständig</span></div>
<div class="flex flex-row items-center gap-2"><div class="rounded-full size-4 bg-purple-600"></div><span>Überstunden</span></div>
<div class="flex flex-row items-center gap-2"><div class="rounded-full size-4 bg-neutral-400"></div><span>Keine Buchungen</span></div>
</div>
}
templ CheckboxComponent(pNr int, label string) {
{{ id := fmt.Sprintf("pdf-%d", pNr) }}
<div class="inline-flex items-center">
<label class="flex items-center cursor-pointer relative" for={ id }>
<input type="checkbox" name="employe_list" value={ pNr } id={ id } class="peer h-5 w-5 cursor-pointer transition-all appearance-none rounded border border-slate-800 checked:bg-slate-800 checked:border-slate-800"/>
<span class="absolute text-white opacity-0 peer-checked:opacity-100 top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor" stroke="currentColor" stroke-width="1">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</span>
</label> <label class="cursor-pointer ml-2 text-slate-600 select-none" for={ id }>{ label }</label>
</div>
}

View File

@@ -1,9 +1,12 @@
package templates package templates
// this file includes the basic pages, that have no further complexity,
// like the login and settings page, the more complex pages have their own files
import "arbeitszeitmessung/models" import "arbeitszeitmessung/models"
import "arbeitszeitmessung/helper" import "arbeitszeitmessung/helper"
templ Base() { templ BasePage() {
<!DOCTYPE html> <!DOCTYPE html>
<head> <head>
<title>Arbeitszeit</title> <title>Arbeitszeit</title>
@@ -14,7 +17,7 @@ templ Base() {
} }
templ LoginPage(success bool, errorMsg string) { templ LoginPage(success bool, errorMsg string) {
@Base() @BasePage()
<div class="w-full h-[100vh] flex flex-col justify-center items-center"> <div class="w-full h-[100vh] flex flex-col justify-center items-center">
<form method="POST" class="w-9/10 md:w-1/2 flex flex-col gap-4 p-2 mb-2"> <form method="POST" class="w-9/10 md:w-1/2 flex flex-col gap-4 p-2 mb-2">
<h1 class="font-bold uppercase text-xl text-center mb-2">Benutzer Anmelden</h1> <h1 class="font-bold uppercase text-xl text-center mb-2">Benutzer Anmelden</h1>
@@ -31,7 +34,7 @@ templ LoginPage(success bool, errorMsg string) {
templ SettingsPage(status int) { templ SettingsPage(status int) {
{{ user := ctx.Value("user").(models.User) }} {{ user := ctx.Value("user").(models.User) }}
@Base() @BasePage()
@headerComponent() @headerComponent()
<div class="grid-main divide-y-1"> <div class="grid-main divide-y-1">
<form method="POST" class="grid-sub responsive lg:divide-x-1"> <form method="POST" class="grid-sub responsive lg:divide-x-1">
@@ -74,15 +77,3 @@ templ SettingsPage(status int) {
</div> </div>
</div> </div>
} }
templ statusCheckMark(status models.WeekStatus, target models.WeekStatus) {
if status >= target {
<div class="icon-[material-symbols-light--check-circle-outline]"></div>
} else {
<div class="icon-[material-symbols-light--circle-outline]"></div>
}
}
templ LogoutButton() {
<button onclick="logoutUser()" type="button" class="cursor-pointer">Abmelden</button>
}

View File

@@ -1,15 +0,0 @@
package templates
templ headerComponent() {
// {{ user := ctx.Value("user").(models.User) }}
<div class="flex flex-row justify-between md:mx-[10%] py-2 items-center">
<a href="/time">Zeitverwaltung</a>
<a href="/team">Abrechnung</a>
if true {
<a href="/pdf">Monatsabrechnung</a>
<a href="/presence">Anwesenheit</a>
}
<a href="/user/settings">Einstellungen</a>
@LogoutButton()
</div>
}

View File

@@ -1,58 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.960
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func headerComponent() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex flex-row justify-between md:mx-[10%] py-2 items-center\"><a href=\"/time\">Zeitverwaltung</a> <a href=\"/team\">Abrechnung</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if true {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"/pdf\">Monatsabrechnung</a> <a href=\"/presence\">Anwesenheit</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<a href=\"/user/settings\">Einstellungen</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = LogoutButton().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,288 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.960
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "arbeitszeitmessung/models"
import "arbeitszeitmessung/helper"
func Base() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><head><title>Arbeitszeit</title><link rel=\"stylesheet\" href=\"/static/css/styles.css\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><script src=\"/static/script.js\" defer></script></head>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func LoginPage(success bool, errorMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full h-[100vh] flex flex-col justify-center items-center\"><form method=\"POST\" class=\"w-9/10 md:w-1/2 flex flex-col gap-4 p-2 mb-2\"><h1 class=\"font-bold uppercase text-xl text-center mb-2\">Benutzer Anmelden</h1><input name=\"personal_nummer\" placeholder=\"Personalnummer\" type=\"text\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> <input name=\"password\" placeholder=\"Passwort\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !success {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<p class=\"text-red-600 text-sm\">Login fehlgeschlagen, bitte erneut versuchen!</p><p class=\"text-red-600 text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errorMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 25, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button type=\"submit\" class=\"cursor-pointer rounded-md text-neutral-800 p-2 md:px-4 border text-center text-sm hover:text-white transition-colors border-neutral-300 focus:bg-neutral-700 active:bg-neutral-700 hover:bg-neutral-700 disabled:pointer-events-none disabled:opacity-50\">Login</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func SettingsPage(status int) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
user := ctx.Value("user").(models.User)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"grid-main divide-y-1\"><form method=\"POST\" class=\"grid-sub responsive lg:divide-x-1\"><h1 class=\"grid-cell font-bold uppercase text-xl text-center\">Passwort ändern</h1><div class=\"grid-cell col-span-3 flex flex-col gap-2\"><input name=\"password\" placeholder=\"Aktuelles Passwort\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> <input name=\"new_password\" placeholder=\"Neues Passwort\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> <input name=\"new_password_repeat\" placeholder=\"Neues Passwort wiederholen\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
switch {
case status == 401:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"text-red-600 text-sm\">Aktuelles Passwort nicht korrekt!</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case status >= 400:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p class=\"text-red-600 text-sm\">Passwortwechsel fehlgeschlagen, bitte erneut versuchen!</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case status == 202:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"text-accent text-sm\">Passwortänderung erfolgreich</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><div class=\"grid-cell\"><button name=\"action\" value=\"change-pass\" type=\"submit\" class=\"btn\">Ändern</button></div></form><div class=\"grid-sub responsive lg:divide-x-1\"><h1 class=\"grid-cell font-bold uppercase text-xl text-center\">Nutzerdaten</h1><div class=\"grid-cell col-span-3\"><p>Nutzername: <span class=\"text-neutral-500\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 59, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 59, Col: 78}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span></p><p>Personalnummer: <span class=\"text-neutral-500\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(user.PersonalNummer)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 60, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span></p><p>Arbeitszeit pro Tag: <span class=\"text-neutral-500\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(user.ArbeitszeitProTag()))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 61, Col: 108}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></p><p>Arbeitszeit pro Woche: <span class=\"text-neutral-500\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(user.ArbeitszeitProWoche()))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 62, Col: 112}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></p></div><div></div></div><div class=\"grid-sub responsive lg:divide-x-1\"><h1 class=\"grid-cell font-bold uppercase text-xl text-center\">Nutzer abmelden</h1><div class=\"grid-cell col-span-3\"><p>Nutzer von Weboberfläche abmelden.</p></div><div class=\"grid-cell\"><button onclick=\"logoutUser\" type=\"button\" class=\"btn\">Abmelden</button></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func statusCheckMark(status models.WeekStatus, target models.WeekStatus) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if status >= target {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"icon-[material-symbols-light--check-circle-outline]\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"icon-[material-symbols-light--circle-outline]\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
func LogoutButton() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var11 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<button onclick=\"logoutUser()\" type=\"button\" class=\"cursor-pointer\">Abmelden</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,134 +0,0 @@
package templates
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"fmt"
"time"
)
templ PDFForm(teamMembers []models.User) {
@Base()
@headerComponent()
<form class="grid-main divide-y-1" action="pdf/generate" method="get">
<div class="grid-cell col-span-full bg-neutral-300">
<h1 class="text-xl uppercase font-bold">Monatsabrechnung erstellen</h1>
</div>
<div class="grid-sub divide-x-1 responsive">
<div class="grid-cell">Zeitraum wählen</div>
<div class="grid-cell col-span-3">
<label class="block mb-1 text-sm text-neutral-700">Abrechnungsmonat</label>
<input name="start_date" type="date" value={ helper.GetFirstOfMonth(time.Now()).Format(time.DateOnly) } class="btn bg-neutral-100"/>
</div>
<div></div>
</div>
<div class="grid-sub divide-x-1 responsive">
<div class="grid-cell">Mitarbeiter wählen</div>
<div class="grid-cell col-span-3 flex flex-col gap-2">
<div class="flex flex-row gap-2">
<button class="btn" type="button" onclick={ templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("true")) }>Alle</button>
<button class="btn" type="button" onclick={ templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("false")) }>Keine</button>
</div>
for _, member := range teamMembers {
@CheckboxComponent(member.PersonalNummer, fmt.Sprintf("%s %s", member.Vorname, member.Name))
}
</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="submit" name="output" value="download">Einzeln</button>
<button class="btn" type="submit" name="output" value="render" onclick="">Bündel</button>
</div>
</div>
</form>
}
templ CheckboxComponent(pNr int, label string) {
{{ id := fmt.Sprintf("pdf-%d", pNr) }}
<div class="inline-flex items-center">
<label class="flex items-center cursor-pointer relative" for={ id }>
<input type="checkbox" name="employe_list" value={ pNr } id={ id } class="peer h-5 w-5 cursor-pointer transition-all appearance-none rounded border border-slate-800 checked:bg-slate-800 checked:border-slate-800"/>
<span class="absolute text-white opacity-0 peer-checked:opacity-100 top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor" stroke="currentColor" stroke-width="1">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</span>
</label> <label class="cursor-pointer ml-2 text-slate-600 select-none" for={ id }>{ label }</label>
</div>
}
// templ PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) {
// {{
// _, kw := tsStart.ISOWeek()
// noBorder := ""
// }}
// @Base()
// <content class="p-8 relative flex flex-col gap-4 break-after-page">
// <div>
// <h1 class="text-2xl font-bold">{ e.Vorname } { e.Name }</h1>
// <p>Zeitraum: <span>{ tsStart.Format("02.01.2006") }</span> - <span>{ tsEnd.Format("02.01.2006") }</span></p>
// <p>Arbeitszeit: <span>{ helper.FormatDuration(worktime) }</span></p>
// <p>Überstunden: <span>{ helper.FormatDuration(overtime) }</span></p>
// </div>
// <div class="grid grid-rows-6 grid-cols-[3fr_2fr_2fr_2fr_3fr_3fr_3fr] *:not-print:p-2 *:text-center auto-rows-min divide-neutral-300 divide-x-1 divide-y-1">
// <p class="bg-neutral-300 border-neutral-600">{ kw }</p>
// <p class="bg-neutral-300 border-neutral-600">Kommen</p>
// <p class="bg-neutral-300 border-neutral-600">Gehen</p>
// <p class="bg-neutral-300 border-neutral-600">Arbeitsart</p>
// <p class="bg-neutral-300 border-neutral-600">Stunden</p>
// <p class="bg-neutral-300 border-neutral-600">Pause</p>
// <p class="bg-neutral-300 border-neutral-600 border-r-0">Überstunden</p>
// for index, day := range workDays {
// {{
// if index == len(workDays)-1 {
// noBorder = "border-b-0"
// }
// }}
// <p class={ noBorder }>{ day.Date().Format("02.01.2006") }</p>
// <div class={ "grid grid-cols-subgrid col-span-3 " + noBorder }>
// if day.IsWorkDay() {
// {{
// workDay, _ := day.(*models.WorkDay)
// }}
// for bookingI := 0; bookingI < len(workDay.Bookings); bookingI+= 2 {
// <p>{ workDay.Bookings[bookingI].Timestamp.Format("15:04") }</p>
// <p>{ workDay.Bookings[bookingI+1].Timestamp.Format("15:04") }</p>
// <p>{ workDay.Bookings[bookingI].BookingType.Name } </p>
// }
// if workDay.IsKurzArbeit() {
// {{
// timeFrom, timeTo := workDay.GenerateKurzArbeitBookings(e)
// }}
// <p>{ timeFrom.Format("15:04") }</p>
// <p>{ timeTo.Format("15:04") }</p>
// <p>Kurzarbeit</p>
// }
// } else {
// {{
// absentDay, _ := day.(*models.Absence)
// }}
// <p class="col-span-full">{ absentDay.AbwesenheitTyp.Name }</p>
// }
// </div>
// {{ work, pause, overtime := day.GetTimesVirtual(e) }}
// @ColorDuration(work, noBorder)
// @ColorDuration(pause, noBorder)
// @ColorDuration(overtime, noBorder+" border-r-0")
// if day.Date().Weekday() == time.Friday {
// <p class="col-span-full bg-neutral-300">Wochenende</p>
// }
// }
// </div>
// </content>
// }
templ ColorDuration(d time.Duration, classes string) {
{{
color := ""
if d.Abs() < time.Minute {
color = "text-neutral-300"
}
}}
<p class={ color + " " + classes }>{ helper.FormatDurationFill(d, true) }</p>
}

View File

@@ -0,0 +1,48 @@
package templates
// this file has all templates for the /pdf page
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"fmt"
"time"
)
templ PDFForm(teamMembers []models.User) {
@BasePage()
@headerComponent()
<form class="grid-main divide-y-1" action="pdf/generate" method="get">
<div class="grid-cell col-span-full bg-neutral-300">
<h1 class="text-xl uppercase font-bold">Monatsabrechnung erstellen</h1>
</div>
<div class="grid-sub divide-x-1 responsive">
<div class="grid-cell">Zeitraum wählen</div>
<div class="grid-cell col-span-3">
<label class="block mb-1 text-sm text-neutral-700">Abrechnungsmonat</label>
<input name="start_date" type="date" value={ helper.GetFirstOfMonth(time.Now()).Format(time.DateOnly) } class="btn bg-neutral-100"/>
</div>
<div></div>
</div>
<div class="grid-sub divide-x-1 responsive">
<div class="grid-cell">Mitarbeiter wählen</div>
<div class="grid-cell col-span-3 flex flex-col gap-2">
<div class="flex flex-row gap-2">
<button class="btn" type="button" onclick={ templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("true")) }>Alle</button>
<button class="btn" type="button" onclick={ templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("false")) }>Keine</button>
</div>
for _, member := range teamMembers {
@CheckboxComponent(member.PersonalNummer, fmt.Sprintf("%s %s", member.Vorname, member.Name))
}
</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="submit" name="output" value="download">Einzeln</button>
<button class="btn" type="submit" name="output" value="render" onclick="">Bündel</button>
</div>
</div>
</form>
}

View File

@@ -1,335 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.960
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"fmt"
"time"
)
func PDFForm(teamMembers []models.User) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form class=\"grid-main divide-y-1\" action=\"pdf/generate\" method=\"get\"><div class=\"grid-cell col-span-full bg-neutral-300\"><h1 class=\"text-xl uppercase font-bold\">Monatsabrechnung erstellen</h1></div><div class=\"grid-sub divide-x-1 responsive\"><div class=\"grid-cell\">Zeitraum wählen</div><div class=\"grid-cell col-span-3\"><label class=\"block mb-1 text-sm text-neutral-700\">Abrechnungsmonat</label> <input name=\"start_date\" type=\"date\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(helper.GetFirstOfMonth(time.Now()).Format(time.DateOnly))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 21, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"btn bg-neutral-100\"></div><div></div></div><div class=\"grid-sub divide-x-1 responsive\"><div class=\"grid-cell\">Mitarbeiter wählen</div><div class=\"grid-cell col-span-3 flex flex-col gap-2\"><div class=\"flex flex-row gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("true")))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<button class=\"btn\" type=\"button\" onclick=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.ComponentScript = templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("true"))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3.Call)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">Alle</button> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("false")))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button class=\"btn\" type=\"button\" onclick=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.ComponentScript = templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("false"))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4.Call)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">Keine</button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, member := range teamMembers {
templ_7745c5c3_Err = CheckboxComponent(member.PersonalNummer, fmt.Sprintf("%s %s", member.Vorname, member.Name)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</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=\"submit\" name=\"output\" value=\"download\">Einzeln</button> <button class=\"btn\" type=\"submit\" name=\"output\" value=\"render\" onclick=\"\">Bündel</button></div></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func CheckboxComponent(pNr int, label string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
id := fmt.Sprintf("pdf-%d", pNr)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"inline-flex items-center\"><label class=\"flex items-center cursor-pointer relative\" for=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 51, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"><input type=\"checkbox\" name=\"employe_list\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(pNr)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 52, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 52, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" class=\"peer h-5 w-5 cursor-pointer transition-all appearance-none rounded border border-slate-800 checked:bg-slate-800 checked:border-slate-800\"> <span class=\"absolute text-white opacity-0 peer-checked:opacity-100 top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3.5 w-3.5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" stroke=\"currentColor\" stroke-width=\"1\"><path fill-rule=\"evenodd\" d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\" clip-rule=\"evenodd\"></path></svg></span></label> <label class=\"cursor-pointer ml-2 text-slate-600 select-none\" for=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 58, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 58, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</label></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// templ PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) {
// {{
// _, kw := tsStart.ISOWeek()
// noBorder := ""
// }}
// @Base()
// <content class="p-8 relative flex flex-col gap-4 break-after-page">
// <div>
// <h1 class="text-2xl font-bold">{ e.Vorname } { e.Name }</h1>
// <p>Zeitraum: <span>{ tsStart.Format("02.01.2006") }</span> - <span>{ tsEnd.Format("02.01.2006") }</span></p>
// <p>Arbeitszeit: <span>{ helper.FormatDuration(worktime) }</span></p>
// <p>Überstunden: <span>{ helper.FormatDuration(overtime) }</span></p>
// </div>
// <div class="grid grid-rows-6 grid-cols-[3fr_2fr_2fr_2fr_3fr_3fr_3fr] *:not-print:p-2 *:text-center auto-rows-min divide-neutral-300 divide-x-1 divide-y-1">
// <p class="bg-neutral-300 border-neutral-600">{ kw }</p>
// <p class="bg-neutral-300 border-neutral-600">Kommen</p>
// <p class="bg-neutral-300 border-neutral-600">Gehen</p>
// <p class="bg-neutral-300 border-neutral-600">Arbeitsart</p>
// <p class="bg-neutral-300 border-neutral-600">Stunden</p>
// <p class="bg-neutral-300 border-neutral-600">Pause</p>
// <p class="bg-neutral-300 border-neutral-600 border-r-0">Überstunden</p>
// for index, day := range workDays {
// {{
// if index == len(workDays)-1 {
// noBorder = "border-b-0"
// }
// }}
// <p class={ noBorder }>{ day.Date().Format("02.01.2006") }</p>
// <div class={ "grid grid-cols-subgrid col-span-3 " + noBorder }>
// if day.IsWorkDay() {
// {{
// workDay, _ := day.(*models.WorkDay)
// }}
// for bookingI := 0; bookingI < len(workDay.Bookings); bookingI+= 2 {
// <p>{ workDay.Bookings[bookingI].Timestamp.Format("15:04") }</p>
// <p>{ workDay.Bookings[bookingI+1].Timestamp.Format("15:04") }</p>
// <p>{ workDay.Bookings[bookingI].BookingType.Name } </p>
// }
// if workDay.IsKurzArbeit() {
// {{
// timeFrom, timeTo := workDay.GenerateKurzArbeitBookings(e)
// }}
// <p>{ timeFrom.Format("15:04") }</p>
// <p>{ timeTo.Format("15:04") }</p>
// <p>Kurzarbeit</p>
// }
// } else {
// {{
// absentDay, _ := day.(*models.Absence)
// }}
// <p class="col-span-full">{ absentDay.AbwesenheitTyp.Name }</p>
// }
// </div>
// {{ work, pause, overtime := day.GetTimesVirtual(e) }}
// @ColorDuration(work, noBorder)
// @ColorDuration(pause, noBorder)
// @ColorDuration(overtime, noBorder+" border-r-0")
// if day.Date().Weekday() == time.Friday {
// <p class="col-span-full bg-neutral-300">Wochenende</p>
// }
// }
// </div>
// </content>
// }
func ColorDuration(d time.Duration, classes string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var11 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
color := ""
if d.Abs() < time.Minute {
color = "text-neutral-300"
}
var templ_7745c5c3_Var12 = []any{color + " " + classes}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<p class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDurationFill(d, true))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 133, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,10 +1,12 @@
package templates package templates
// this file has all the templates for the /team/presence page
import "arbeitszeitmessung/models" import "arbeitszeitmessung/models"
import "arbeitszeitmessung/helper" import "arbeitszeitmessung/helper"
templ TeamPresencePage(teamPresence map[models.User]bool) { templ TeamPresencePage(teamPresence map[models.User]bool) {
@Base() @BasePage()
@headerComponent() @headerComponent()
<div class="grid-main divide-y-1"> <div class="grid-main divide-y-1">
<div class="grid-sub divide-x-1 bg-neutral-300"> <div class="grid-sub divide-x-1 bg-neutral-300">
@@ -27,3 +29,14 @@ templ TeamPresencePage(teamPresence map[models.User]bool) {
} }
</div> </div>
} }
templ userPresenceComponent(user models.User, present bool) {
<div class="grid-cell group flex flex-row gap-2">
if present {
<div class="h-8 bg-accent rounded-md group-hover:text-black md:text-transparent text-center p-1">Anwesend</div>
} else {
<div class="h-8 bg-red-600 rounded-md group-hover:text-white md:text-transparent text-center p-1">Abwesend</div>
}
<p>{ user.Vorname } { user.Name }</p>
</div>
}

View File

@@ -1,110 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.960
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "arbeitszeitmessung/models"
import "arbeitszeitmessung/helper"
func TeamPresencePage(teamPresence map[models.User]bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"grid-main divide-y-1\"><div class=\"grid-sub divide-x-1 bg-neutral-300\"><h2 class=\"grid-cell font-bold uppercase text-xl\">Mitarbeiter</h2></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for user, present := range teamPresence {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"grid-sub\"><div class=\"grid-cell flex flex-row gap-2 col-span-2 md:col-span-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = timeGaugeComponent(helper.BoolToInt8(present)*100-1, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/presencePage.templ`, Line: 17, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/presencePage.templ`, Line: 17, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div><div class=\"grid-cell col-span-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if present {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"text-neutral-500\">Anwesend</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"text-neutral-500\">Abwesend</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,5 +1,7 @@
package templates package templates
// this file has all the templates for the team/report page
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
@@ -8,11 +10,11 @@ import (
"time" "time"
) )
templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) { templ ReportPage(weeks []models.WorkWeek, userWeek models.WorkWeek) {
@Base() @BasePage()
@headerComponent() @headerComponent()
<div class="grid-main divide-y-1"> <div class="grid-main divide-y-1 @container">
<div class="grid-sub lg:divide-x-1 max-md:divide-y-1 responsive @container"> <div class="grid-sub lg:divide-x-1 max-md:divide-y-1 responsive">
<div class="grid-cell col-span-full bg-neutral-300 lg:border-0"> <div class="grid-cell col-span-full bg-neutral-300 lg:border-0">
<h2 class="text-xl uppercase font-bold">Eigene Abrechnung</h2> <h2 class="text-xl uppercase font-bold">Eigene Abrechnung</h2>
</div> </div>
@@ -34,7 +36,7 @@ templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
year, kw := week.WeekStart.ISOWeek() year, kw := week.WeekStart.ISOWeek()
progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100 progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100
}} }}
<div class="employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1 @container"> <div class="employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1">
<div class="grid-cell flex flex-col max-md:bg-neutral-300 gap-2"> <div class="grid-cell flex flex-col max-md:bg-neutral-300 gap-2">
if !onlyAccept { if !onlyAccept {
<div class="lg:hidden"> <div class="lg:hidden">
@@ -45,6 +47,12 @@ templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
<div class="grid grid-cols-5 gap-2 lg:grid-cols-1"> <div class="grid grid-cols-5 gap-2 lg:grid-cols-1">
if !onlyAccept { if !onlyAccept {
<div class="col-span-2"> <div class="col-span-2">
if week.CheckStatus() == models.WeekStatusCorrected {
<span class="flex flex-row gap-2 items-center">
<div class="icon-[material-symbols-light--edit-calendar-rounded]"></div>
laufende Korrektur
</span>
}
<span class="flex flex-row gap-2 items-center"> <span class="flex flex-row gap-2 items-center">
@statusCheckMark(week.CheckStatus(), models.WeekStatusSent) @statusCheckMark(week.CheckStatus(), models.WeekStatusSent)
Gesendet Gesendet
@@ -58,7 +66,7 @@ templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
<div class="flex flex-row gap-2 col-span-3"> <div class="flex flex-row gap-2 col-span-3">
@timeGaugeComponent(int8(progress), false) @timeGaugeComponent(int8(progress), false)
<div> <div>
<p>Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDuration(week.Worktime)) }</p> <p>Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Worktime, true)) }</p>
<p>Überstunden: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true)) }</p> <p>Überstunden: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true)) }</p>
</div> </div>
</div> </div>
@@ -144,3 +152,41 @@ templ weekDayTypeSwitcher(day models.IWorkDay) {
<div>{ day.ToString() }</div> <div>{ day.ToString() }</div>
} }
} }
templ weekPicker(weekStart time.Time) {
{{ year, kw := weekStart.ISOWeek() }}
<form method="get" class="flex flex-row gap-4 items-center justify-around">
<input type="date" class="hidden" name="submission_date" value={ weekStart.Format(time.DateOnly) }/>
<button onclick={ templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "-1") } class="btn">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="chevron-left size-4 mx-auto" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0"></path>
</svg>
</button>
<p class="whitespace-nowrap">KW { fmt.Sprintf("%02d, %d", kw, year) }</p>
<button disabled?={ time.Since(weekStart) < 24*7*time.Hour } onclick={ templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "1") } class="btn disabled:pointer-events-none disabled:opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="chevron-right size-4 mx-auto" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708"></path>
</svg>
</button>
</form>
}
templ workDayWeekComponent(workDay *models.WorkDay) {
if !workDay.RequiresAction() {
<div class="flex flex-row gap-2 items-center">
<span class="icon-[material-symbols-light--schedule-outline] flex-shrink-0"></span>
switch {
case !workDay.IsEmpty():
<span>{ workDay.TimeFrom.Format("15:04") }</span>
<span>-</span>
<span>{ workDay.TimeTo.Format("15:04") }</span>
case workDay.IsKurzArbeit():
<span>Kurzarbeit</span>
default:
<p>Keine Anwesenheit</p>
}
</div>
} else {
<p class="text-red-600">Bitte anpassen</p>
}
}

View File

@@ -1,545 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.960
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"fmt"
"strconv"
"time"
)
func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"grid-main divide-y-1\"><div class=\"grid-sub lg:divide-x-1 max-md:divide-y-1 responsive @container\"><div class=\"grid-cell col-span-full bg-neutral-300 lg:border-0\"><h2 class=\"text-xl uppercase font-bold\">Eigene Abrechnung</h2></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = workWeekComponent(userWeek, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(weeks) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"grid-cell col-span-full bg-neutral-300\"><h2 class=\"text-xl uppercase font-bold\">Abrechnung Mitarbeiter</h2></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
for _, week := range weeks {
templ_7745c5c3_Err = workWeekComponent(week, true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
year, kw := week.WeekStart.ISOWeek()
progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1 @container\"><div class=\"grid-cell flex flex-col max-md:bg-neutral-300 gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !onlyAccept {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"lg:hidden\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = weekPicker(week.WeekStart).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"font-bold uppercase\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Vorname)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 44, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 44, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</p><div class=\"grid grid-cols-5 gap-2 lg:grid-cols-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !onlyAccept {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"col-span-2\"><span class=\"flex flex-row gap-2 items-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = statusCheckMark(week.CheckStatus(), models.WeekStatusSent).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "Gesendet</span> <span class=\"flex flex-row gap-2 items-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = statusCheckMark(week.CheckStatus(), models.WeekStatusAccepted).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Akzeptiert</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"flex flex-row gap-2 col-span-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = timeGaugeComponent(int8(progress), false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div><p>Arbeitszeit: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDuration(week.Worktime)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 61, Col: 79}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p><p>Überstunden: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 62, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p></div></div></div></div><div class=\"grid-cell col-span-3 flex flex-col @7xl:grid @7xl:grid-cols-5 gap-2 py-4 content-baseline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, day := range week.Days {
templ_7745c5c3_Err = defaultWeekDayComponent(week.User, day).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div><div class=\"grid-cell flex flex-col gap-2 justify-between\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if onlyAccept {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<p class=\"text-sm\"><span class=\"\">Woche:</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d-%d", kw, year))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 74, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"max-md:hidden\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = weekPicker(week.WeekStart).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<form method=\"post\" class=\"flex flex-col gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
week.CheckStatus()
method := "accept"
if !onlyAccept {
method = "send"
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<input type=\"hidden\" name=\"method\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 88, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"> <input type=\"hidden\" name=\"user\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(week.User.PersonalNummer))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 89, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"> <input type=\"hidden\" name=\"week\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(week.WeekStart.Format(time.DateOnly))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 90, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if onlyAccept {
if week.Status == models.WeekStatusDifferences {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<p class=\"text-red-600 text-sm\">Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " <button type=\"submit\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if week.Status == models.WeekStatusDifferences {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " class=\"btn\">Bestätigen</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
switch {
case week.RequiresAction():
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<p class=\"text-sm text-red-500\">bitte zuerst Buchungen anpassen</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case time.Since(week.WeekStart) < 24*7*time.Hour:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<p class=\"text-sm text-red-500\">Die Woche kann erst am nächsten Montag gesendet werden!</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case week.Status == models.WeekStatusNone:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<p class=\"text-sm\">an Vorgesetzten senden</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case week.Status == models.WeekStatusSent:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<p class=\"text-sm\">an Vorgesetzten gesendet</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case week.Status == models.WeekStatusAccepted:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<p class=\"text-sm\">vom Vorgesetzten bestätigt</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " <button")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if week.Status < models.WeekStatusSent {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " type=\"submit\" class=\"btn\">Korrigieren</button> <button")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if time.Since(week.WeekStart) < 24*7*time.Hour || week.Status >= models.WeekStatusSent || week.RequiresAction() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " type=\"submit\" class=\"btn\">Senden</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</form></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var11 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<div class=\"flex flex-row gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = timeGaugeComponent(day.GetDayProgress(u), false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<div class=\"flex flex-col\"><p class=\"\"><span class=\"font-bold uppercase hidden md:inline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatGermanDayOfWeek(day.Date()))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 121, Col: 108}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, ":</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 121, Col: 152}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
work, pause, _ := day.GetTimes(u, models.WorktimeBaseDay, false)
if day.IsWorkDay() || day.GetDayProgress(u) < 100 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<div class=\"flex flex-row gap-2\"><span class=\"text-accent\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(work))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 125, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</span> <span class=\"text-neutral-500\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(pause))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 126, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = weekDayTypeSwitcher(day).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func weekDayTypeSwitcher(day models.IWorkDay) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
if templ_7745c5c3_Var16 == nil {
templ_7745c5c3_Var16 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch day.Type() {
case models.DayTypeWorkday:
workDay, _ := day.(*models.WorkDay)
templ_7745c5c3_Err = workDayWeekComponent(workDay).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case models.DayTypeCompound:
for _, c := range day.(*models.CompoundDay).DayParts {
templ_7745c5c3_Err = weekDayTypeSwitcher(c).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
default:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(day.ToString())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 144, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,54 +0,0 @@
package templates
import (
"arbeitszeitmessung/models"
"fmt"
"time"
)
templ weekPicker(weekStart time.Time) {
{{ year, kw := weekStart.ISOWeek() }}
<form method="get" class="flex flex-row gap-4 items-center justify-around">
<input type="date" class="hidden" name="submission_date" value={ weekStart.Format(time.DateOnly) }/>
<button onclick={ templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "-1") } class="btn">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="chevron-left size-4 mx-auto" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0"></path>
</svg>
</button>
<p class="whitespace-nowrap">KW { fmt.Sprintf("%02d, %d", kw, year) }</p>
<button disabled?={ time.Since(weekStart) < 24*7*time.Hour } onclick={ templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "1") } class="btn disabled:pointer-events-none disabled:opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="chevron-right size-4 mx-auto" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708"></path>
</svg>
</button>
</form>
}
templ workDayWeekComponent(workDay *models.WorkDay) {
if !workDay.RequiresAction() {
<div class="flex flex-row gap-2 items-center">
<span class="icon-[material-symbols-light--schedule-outline] flex-shrink-0"></span>
switch {
case !workDay.TimeFrom.Equal(workDay.TimeTo):
<span>{ workDay.TimeFrom.Format("15:04") }</span>
<span>-</span>
<span>{ workDay.TimeTo.Format("15:04") }</span>
default:
<p>Keine Anwesenheit</p>
}
</div>
} else {
<p class="text-red-600">Bitte anpassen</p>
}
}
templ userPresenceComponent(user models.User, present bool) {
<div class="grid-cell group flex flex-row gap-2">
if present {
<div class="h-8 bg-accent rounded-md group-hover:text-black md:text-transparent text-center p-1">Anwesend</div>
} else {
<div class="h-8 bg-red-600 rounded-md group-hover:text-white md:text-transparent text-center p-1">Abwesend</div>
}
<p>{ user.Vorname } { user.Name }</p>
</div>
}

View File

@@ -1,265 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.960
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"arbeitszeitmessung/models"
"fmt"
"time"
)
func weekPicker(weekStart time.Time) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
year, kw := weekStart.ISOWeek()
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form method=\"get\" class=\"flex flex-row gap-4 items-center justify-around\"><input type=\"date\" class=\"hidden\" name=\"submission_date\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(weekStart.Format(time.DateOnly))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 12, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "-1"))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<button onclick=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.ComponentScript = templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "-1")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3.Call)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"btn\"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" class=\"chevron-left size-4 mx-auto\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0\"></path></svg></button><p class=\"whitespace-nowrap\">KW ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d, %d", kw, year))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 18, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "1"))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<button")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if time.Since(weekStart) < 24*7*time.Hour {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " onclick=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.ComponentScript = templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "1")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5.Call)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" class=\"btn disabled:pointer-events-none disabled:opacity-50\"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" class=\"chevron-right size-4 mx-auto\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708\"></path></svg></button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func workDayWeekComponent(workDay *models.WorkDay) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if !workDay.RequiresAction() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"flex flex-row gap-2 items-center\"><span class=\"icon-[material-symbols-light--schedule-outline] flex-shrink-0\"></span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
switch {
case !workDay.TimeFrom.Equal(workDay.TimeTo):
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.TimeFrom.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 33, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span> <span>-</span> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.TimeTo.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<p>Keine Anwesenheit</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<p class=\"text-red-600\">Bitte anpassen</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
func userPresenceComponent(user models.User, present bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"grid-cell group flex flex-row gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if present {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"h-8 bg-accent rounded-md group-hover:text-black md:text-transparent text-center p-1\">Anwesend</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"h-8 bg-red-600 rounded-md group-hover:text-white md:text-transparent text-center p-1\">Abwesend</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 52, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 52, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -2,24 +2,16 @@ package templates
import ( import (
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"fmt"
"strconv" "strconv"
"time" "time"
) )
templ lineComponent() { templ changeButtonComponent(id string, workDay bool, disabled bool) {
<div class="flex flex-col w-2 py-2 items-center text-accent print:hidden"> if disabled {
<svg class="size-2" viewBox="0 0 24 24" fill="currentColor"> <button class="h-10 change-button-component btn w-auto group/button" type="button" disabled>
<polygon points="12,2 22,12 12,22 2,12"></polygon> <p class="hidden md:block group-[.edit]/button:hidden">Ändern</p>
</svg> </button>
<div class="w-[2px] bg-accent flex-grow -my-1"></div> } else {
<svg class="size-2" viewBox="0 0 24 24" fill="currentColor">
<polygon points="12,2 22,12 12,22 2,12"></polygon>
</svg>
</div>
}
templ changeButtonComponent(id string, workDay bool) {
<button class="h-10 change-button-component btn w-auto group/button" type="button" onclick={ templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), id, workDay) }> <button class="h-10 change-button-component btn w-auto group/button" type="button" onclick={ templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), id, workDay) }>
<p class="hidden md:block group-[.edit]/button:hidden">Ändern</p> <p class="hidden md:block group-[.edit]/button:hidden">Ändern</p>
<p class="hidden group-[.edit]/button:md:block">Speichern</p> <p class="hidden group-[.edit]/button:md:block">Speichern</p>
@@ -30,35 +22,6 @@ templ changeButtonComponent(id string, workDay bool) {
</button> </button>
<button class="h-10 hidden group-[.edit]:flex btn basis-[content] items-center" onclick={ templ.JSFuncCall("clearEditState") }><span class="size-5 icon-[material-symbols-light--cancel-outline]"></span></button> <button class="h-10 hidden group-[.edit]:flex btn basis-[content] items-center" onclick={ templ.JSFuncCall("clearEditState") }><span class="size-5 icon-[material-symbols-light--cancel-outline]"></span></button>
} }
templ timeGaugeComponent(progress int8, today bool) {
{{
var bgColor string
switch {
case (0 > progress):
bgColor = "bg-red-600"
break
case (progress > 0 && progress < 95):
bgColor = "bg-orange-500"
break
case (95 <= progress && progress <= 105):
bgColor = "bg-accent"
break
case (progress > 105):
bgColor = "bg-purple-600"
break
default:
bgColor = "bg-neutral-400"
break
}
}}
if today {
<div class="flex-start flex w-2 h-full overflow-hidden rounded-full bg-neutral-300 print:hidden">
<div class={ "flex w-full items-center justify-center overflow-hidden rounded-full", bgColor } style={ fmt.Sprintf("height: %d%%", int(progress)) }></div>
</div>
} else {
<div class={ "w-2 h-full bg-accent rounded-md flex-shrink-0", bgColor }></div>
}
} }
templ newAbsenceComponent() { templ newAbsenceComponent() {
@@ -120,23 +83,31 @@ templ newBookingComponent(d models.IWorkDay) {
templ bookingComponent(booking models.Booking) { templ bookingComponent(booking models.Booking) {
<div> <div>
<p class="text-neutral-500 edit-box"> <p class={ "text-neutral-500 edit-box", templ.KV("text-red-500", !booking.Valid) }>
<span class="text-black group-[.edit]:hidden inline">{ booking.Timestamp.Format("15:04") }</span> <span class="text-black group-[.edit]:hidden inline">{ booking.Timestamp.Format("15:04") }</span>
<input disabled name={ "booking_" + strconv.Itoa(booking.CounterId) } type="time" value={ booking.Timestamp.Format("15:04") } class="text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer"/> <input disabled name={ "booking_" + strconv.Itoa(booking.CounterId) } type="time" value={ booking.Timestamp.Format("15:04") } class="text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer"/>
{ booking.GetBookingType() } { booking.GetBookingType() }
</p> if !booking.Valid {
if booking.IsSubmittedAndChecked() { fehlerhafte Buchung, wird nicht zur Berechnung verwendet!
<p>submitted</p>
} }
</p>
</div> </div>
} }
templ LegendComponent() { templ workdayComponent(workDay *models.WorkDay) {
<div class="flex flex-row gap-4 md:mx-[10%] print:hidden"> if workDay.IsEmpty() && !workDay.IsKurzArbeit() {
<div class="flex flex-row items-center gap-2"><div class="rounded-full size-4 bg-red-600"></div><span>Fehler</span></div> <p class="text group-[.edit]:hidden">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p>
<div class="flex flex-row items-center gap-2"><div class="rounded-full size-4 bg-orange-500"></div><span>Arbeitszeit unter regulär</span></div> } else {
<div class="flex flex-row items-center gap-2"><div class="rounded-full size-4 bg-accent"></div><span>Arbeitszeit vollständig</span></div> if workDay.IsKurzArbeit() {
<div class="flex flex-row items-center gap-2"><div class="rounded-full size-4 bg-purple-600"></div><span>Überstunden</span></div> @absenceComponent(workDay.GetKurzArbeit(), true)
<div class="flex flex-row items-center gap-2"><div class="rounded-full size-4 bg-neutral-400"></div><span>Keine Buchungen</span></div> }
</div> for _, booking := range workDay.Bookings {
@bookingComponent(booking)
}
<input type="hidden" name="select_kommen" value={ len(workDay.Bookings) > 0 && workDay.Bookings[len(workDay.Bookings)-1].CheckInOut%2 == 0 }/>
}
}
templ holidayComponent(d models.IWorkDay) {
<p>{ d.ToString() }</p>
} }

View File

@@ -1,647 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.960
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"arbeitszeitmessung/models"
"fmt"
"strconv"
"time"
)
func lineComponent() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex flex-col w-2 py-2 items-center text-accent print:hidden\"><svg class=\"size-2\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><polygon points=\"12,2 22,12 12,22 2,12\"></polygon></svg><div class=\"w-[2px] bg-accent flex-grow -my-1\"></div><svg class=\"size-2\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><polygon points=\"12,2 22,12 12,22 2,12\"></polygon></svg></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func changeButtonComponent(id string, workDay bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), id, workDay))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<button class=\"h-10 change-button-component btn w-auto group/button\" type=\"button\" onclick=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.ComponentScript = templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), id, workDay)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3.Call)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><p class=\"hidden md:block group-[.edit]/button:hidden\">Ändern</p><p class=\"hidden group-[.edit]/button:md:block\">Speichern</p><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" class=\"w-4 h-4 md:hidden\"><path class=\"group-[.edit]/button:hidden md:hidden\" d=\"M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325\"></path> <path class=\"hidden group-[.edit]/button:block md:hidden\" d=\"M12.736 3.97a.733.733 0 0 1 j1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425z\"></path></svg></button> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("clearEditState"))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<button class=\"h-10 hidden group-[.edit]:flex btn basis-[content] items-center\" onclick=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.ComponentScript = templ.JSFuncCall("clearEditState")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4.Call)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><span class=\"size-5 icon-[material-symbols-light--cancel-outline]\"></span></button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func timeGaugeComponent(progress int8, today bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var bgColor string
switch {
case (0 > progress):
bgColor = "bg-red-600"
break
case (progress > 0 && progress < 95):
bgColor = "bg-orange-500"
break
case (95 <= progress && progress <= 105):
bgColor = "bg-accent"
break
case (progress > 105):
bgColor = "bg-purple-600"
break
default:
bgColor = "bg-neutral-400"
break
}
if today {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"flex-start flex w-2 h-full overflow-hidden rounded-full bg-neutral-300 print:hidden\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 = []any{"flex w-full items-center justify-center overflow-hidden rounded-full", bgColor}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var6).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("height: %d%%", int(progress)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 57, Col: 149}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
var templ_7745c5c3_Var9 = []any{"w-2 h-full bg-accent rounded-md flex-shrink-0", bgColor}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
func newAbsenceComponent() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var11 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"no-booking-component hidden group-[.edit]:flex flex-col gap-2 align-center \">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), 0, false))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<button type=\"button\" name=\"absence\" onclick=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 templ.ComponentScript = templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), 0, false)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12.Call)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"btn border-neutral-500\">Neue Abwesenheit</button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func absenceComponent(a *models.Absence, isKurzarbeit bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
if templ_7745c5c3_Var13 == nil {
templ_7745c5c3_Var13 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
editBox := ""
if isKurzarbeit {
editBox = "edit-box"
}
var templ_7745c5c3_Var14 = []any{"flex flex-row items-center gap-2", editBox}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = absentInput(a).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<p class=\"whitespace-nowrap group-[.edit]:ml-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(a.ToString())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 82, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if a.IsMultiDay() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"text-neutral-500\">bis ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateTo.Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 84, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</p><div class=\"w-full\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if isKurzarbeit {
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), "time-"+a.Date().Format(time.DateOnly), false))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<button type=\"button\" onclick=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 templ.ComponentScript = templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), "time-"+a.Date().Format(time.DateOnly), false)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18.Call)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"hidden btn border-0 rounded-none grow-0 w-auto group-[.edit]:inline\">Bearbeiten</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func absentInput(a *models.Absence) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
if templ_7745c5c3_Var19 == nil {
templ_7745c5c3_Var19 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<input type=\"hidden\" name=\"date_from\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateFrom.Format(time.DateOnly))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 95, Col: 79}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\"> <input type=\"hidden\" name=\"date_to\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateTo.Format(time.DateOnly))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 96, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"> <input type=\"hidden\" name=\"aw_type\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(a.AbwesenheitTyp.Id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 97, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\"> <input type=\"hidden\" name=\"aw_id\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(a.CounterId)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 98, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// js function to select the right entry
func newBookingComponent(d models.IWorkDay) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var24 := templ.GetChildren(ctx)
if templ_7745c5c3_Var24 == nil {
templ_7745c5c3_Var24 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div class=\"new-booking-component hidden group-[.edit]:flex flex-row gap-2 items-center edit-box border-dashed\" id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs("nb" + d.Date().Format(time.DateOnly))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 103, Col: 155}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\"><input name=\"timestamp\" type=\"time\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(time.Now().Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 104, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" class=\"text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer\"> <input name=\"date\" type=\"hidden\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(d.Date().Format(time.DateOnly))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 105, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"><div class=\"relative\"><select class=\"cursor-pointer appearance-none\" name=\"check_in_out\"><option value=\"0\" disabled>Kommen/Gehen</option> <option value=\"3\">Kommen</option> <option value=\"4\">Gehen</option></select> <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.2\" stroke=\"currentColor\" class=\"h-5 w-5 ml-1 absolute right-1 top-[0.125rem] text-slate-700\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9\"></path></svg></div><div class=\"w-full\"></div><button name=\"action\" value=\"add\" type=\"submit\" class=\"hidden btn border-0 rounded-none grow-0 w-auto group-[.edit]:inline\"><span class=\"hidden md:inline\">Hinzufügen</span><span class=\"md:hidden\">+</span></button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func bookingComponent(booking models.Booking) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var28 := templ.GetChildren(ctx)
if templ_7745c5c3_Var28 == nil {
templ_7745c5c3_Var28 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div><p class=\"text-neutral-500 edit-box\"><span class=\"text-black group-[.edit]:hidden inline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(booking.Timestamp.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 124, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span> <input disabled name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs("booking_" + strconv.Itoa(booking.CounterId))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 125, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" type=\"time\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(booking.Timestamp.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 125, Col: 126}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" class=\"text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(booking.GetBookingType())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 126, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if booking.IsSubmittedAndChecked() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<p>submitted</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func LegendComponent() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var33 := templ.GetChildren(ctx)
if templ_7745c5c3_Var33 == nil {
templ_7745c5c3_Var33 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<div class=\"flex flex-row gap-4 md:mx-[10%] print:hidden\"><div class=\"flex flex-row items-center gap-2\"><div class=\"rounded-full size-4 bg-red-600\"></div><span>Fehler</span></div><div class=\"flex flex-row items-center gap-2\"><div class=\"rounded-full size-4 bg-orange-500\"></div><span>Arbeitszeit unter regulär</span></div><div class=\"flex flex-row items-center gap-2\"><div class=\"rounded-full size-4 bg-accent\"></div><span>Arbeitszeit vollständig</span></div><div class=\"flex flex-row items-center gap-2\"><div class=\"rounded-full size-4 bg-purple-600\"></div><span>Überstunden</span></div><div class=\"flex flex-row items-center gap-2\"><div class=\"rounded-full size-4 bg-neutral-400\"></div><span>Keine Buchungen</span></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,5 +1,9 @@
package templates package templates
// this files includes the largest templates from the time page,
// because this page is so complex the smaller components are in
// the timeComponents.templ file
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
@@ -10,7 +14,7 @@ import (
templ TimePage(workDays []models.WorkDay, lastSub time.Time) { templ TimePage(workDays []models.WorkDay, lastSub time.Time) {
{{ allDays := ctx.Value("days").([]models.IWorkDay) }} {{ allDays := ctx.Value("days").([]models.IWorkDay) }}
@Base() @BasePage()
@headerComponent() @headerComponent()
<div class="grid-main divide-y-1"> <div class="grid-main divide-y-1">
@inputForm() @inputForm()
@@ -21,7 +25,7 @@ templ TimePage(workDays []models.WorkDay, lastSub time.Time) {
} }
} }
</div> </div>
@LegendComponent() @legendComponent()
} }
templ inputForm() { templ inputForm() {
@@ -139,7 +143,10 @@ templ defaultDayComponent(day models.IWorkDay) {
</form> </form>
</div> </div>
<div class="grid-cell flex flex-row gap-2 items-end "> <div class="grid-cell flex flex-row gap-2 items-end ">
@changeButtonComponent("time-"+day.Date().Format(time.DateOnly), true) @changeButtonComponent("time-"+day.Date().Format(time.DateOnly), true, day.IsSubmittedAndAccepted())
if day.IsSubmittedAndAccepted() {
<span class="size-6 my-2 icon-[material-symbols-light--lock]"></span>
}
</div> </div>
</div> </div>
} }
@@ -160,21 +167,3 @@ templ timeDayTypeSwitch(day models.IWorkDay, fromCompound bool) {
<p>{ day.ToString() }</p> <p>{ day.ToString() }</p>
} }
} }
templ workdayComponent(workDay *models.WorkDay) {
if len(workDay.Bookings) < 1 {
<p class="text group-[.edit]:hidden">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p>
} else {
if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 {
@absenceComponent(workDay.GetKurzArbeit(), true)
}
for _, booking := range workDay.Bookings {
@bookingComponent(booking)
}
<input type="hidden" name="select_kommen" value={ len(workDay.Bookings) > 0 && workDay.Bookings[len(workDay.Bookings)-1].CheckInOut%2 == 0 }/>
}
}
templ holidayComponent(d models.IWorkDay) {
<p>{ d.ToString() }</p>
}

View File

@@ -1,623 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.960
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"net/url"
"strconv"
"time"
)
func TimePage(workDays []models.WorkDay, lastSub time.Time) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
allDays := ctx.Value("days").([]models.IWorkDay)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"grid-main divide-y-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = inputForm().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, day := range allDays {
templ_7745c5c3_Err = defaultDayComponent(day).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if day.Date().Weekday() == time.Monday {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"grid-sub responsive bg-neutral-300 h-2\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = LegendComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func inputForm() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
urlParams := ctx.Value("urlParams").(url.Values)
user := ctx.Value("user").(models.User)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"sticky top-0 z-100 grid-sub divide-y-1\"><div class=\"grid-sub divide-x-1 bg-neutral-300 responsive\"><div class=\"grid-cell md:col-span-1 max-md:grid grid-cols-2\"><p class=\"font-bold uppercase\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname + " " + user.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 35, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</p><div class=\"justify-self-end\"><p class=\"text-sm\">Überstunden</p><p class=\"text-accent\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Overtime)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 38, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p></div></div><form id=\"timeRangeForm\" method=\"GET\" class=\"grid-cell flex flex-row md:col-span-3 gap-2 \">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = lineComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"flex flex-col gap-2 justify-between grow-1\"><input type=\"date\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(urlParams.Get("time_from"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 44, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" name=\"time_from\" class=\"btn bg-neutral-100\" placeholder=\"Zeitraum von...\"> <input type=\"date\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(urlParams.Get("time_to"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 45, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" name=\"time_to\" class=\"btn bg-neutral-100\" placeholder=\"Zeitraum bis...\"></div></form><div class=\"grid-cell content-end\"><button type=\"submit\" form=\"timeRangeForm\" class=\"btn bg-neutral-100 hover:bg-neutral-700 color-neutral-700\"><p class=\"\">Anzeigen</p></button></div></div><form id=\"absence_form\" method=\"POST\" action=\"/absence\" class=\"grid-sub responsive scroll-m-2 bg-neutral-300 hidden\"><input type=\"hidden\" name=\"aw_id\" value=\"\"><div class=\"grid-cell border-r-1\"><p class=\"font-bold uppercase\">Abwesenheit</p></div><div class=\"grid-cell\"><label class=\"block mb-1 text-sm text-neutral-700\">Abwesenheitsart</label><div class=\"relative\"><select name=\"aw_type\" class=\"btn appearance-none cursor-pointer bg-neutral-100\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, absence := range models.GetAbsenceTypesCached() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(int(absence.Id)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 62, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(absence.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 62, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</select> <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.2\" stroke=\"currentColor\" class=\"h-5 w-5 ml-1 absolute top-2.5 right-2.5 text-slate-700\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9\"></path></svg></div></div><div class=\"grid-cell\"><label class=\"block mb-1 text-sm text-neutral-700\">Abwesenheit ab</label> <input name=\"date_from\" type=\"date\" class=\"btn bg-neutral-100\"></div><div class=\"grid-cell border-r-1\"><label class=\"block mb-1 text-sm text-neutral-700\">Abwesenheit bis</label> <input name=\"date_to\" type=\"date\" class=\"btn bg-neutral-100\"></div><div class=\"grid-cell flex flex-row items-end\"><div class=\"flex flex-row gap-2 w-full\"><button name=\"action\" value=\"insert\" type=\"submit\" class=\"bg-neutral-100 btn hover:bg-neutral-700\">Speichern</button> <button name=\"action\" value=\"delete\" type=\"submit\" class=\"bg-neutral-100 btn hover:bg-red-700 flex basis-[content] items-center\"><span class=\"size-5 icon-[material-symbols-light--delete-outline]\"></span></button></div></div></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func defaultDayComponent(day models.IWorkDay) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
user := ctx.Value("user").(models.User)
justify := "justify-center"
if day.IsWorkDay() && !day.IsEmpty() {
justify = "justify-between"
}
var templ_7745c5c3_Var10 = []any{"grid-sub divide-x-1 hover:bg-neutral-200 transition-colors group"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var10).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"><div class=\"grid-cell md:col-span-1 flex flex-row gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = timeGaugeComponent(day.GetDayProgress(user), day.Date().Equal(time.Now().Truncate(24*time.Hour))).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div><p><span class=\"font-bold uppercase hidden md:inline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatGermanDayOfWeek(day.Date()))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, ":</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 142}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if day.IsWorkDay() {
work, pause, overtime := day.GetTimes(user, models.WorktimeBaseDay, true)
work = day.GetWorktime(user, models.WorktimeBaseDay, false)
if day.RequiresAction() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<p class=\"text-red-600\">Bitte anpassen</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
if work > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p class=\" text-sm mt-1\">Arbeitszeit:</p><p class=\"text-accent flex flex-row items-center\"><span class=\"icon-[material-symbols-light--schedule-outline]\"></span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(work))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 113, Col: 155}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if pause > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<p class=\"text-neutral-500 flex flex-row items-center\"><span class=\"icon-[material-symbols-light--motion-photos-paused-outline]\"></span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(pause))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 116, Col: 173}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !day.IsEmpty() && overtime != 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<p class=\"text-neutral-500 flex flex-row items-center\"><span class=\"icon-[material-symbols-light--more-time]\"></span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(overtime))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 121, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</div></div><div class=\"all-booking-component grid-cell flex flex-row md:col-span-3 col-span-2 gap-2 w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = lineComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 = []any{"bookings flex flex-col gap-2 w-full", justify}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<form id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("time-" + day.Date().Format(time.DateOnly))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 130, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var17).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" method=\"post\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if day.GetDayProgress(user) < 100 || day.IsWorkDay() {
templ_7745c5c3_Err = newAbsenceComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = timeDayTypeSwitch(day, true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = newBookingComponent(day).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = timeDayTypeSwitch(day, true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<input type=\"hidden\" name=\"action\" value=\"change\"><!-- default action value for ändern button --></form></div><div class=\"grid-cell flex flex-row gap-2 items-end\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = changeButtonComponent("time-"+day.Date().Format(time.DateOnly), true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func timeDayTypeSwitch(day models.IWorkDay, fromCompound bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
if templ_7745c5c3_Var20 == nil {
templ_7745c5c3_Var20 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch day.Type() {
case models.DayTypeWorkday:
workDay, _ := day.(*models.WorkDay)
templ_7745c5c3_Err = workdayComponent(workDay).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case models.DayTypeAbsence:
absentDay, _ := day.(*models.Absence)
templ_7745c5c3_Err = absenceComponent(absentDay, fromCompound).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case models.DayTypeCompound:
for _, c := range day.(*models.CompoundDay).DayParts {
templ_7745c5c3_Err = timeDayTypeSwitch(c, true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
default:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(day.ToString())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 160, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
func workdayComponent(workDay *models.WorkDay) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
if templ_7745c5c3_Var22 == nil {
templ_7745c5c3_Var22 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if len(workDay.Bookings) < 1 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<p class=\"text group-[.edit]:hidden\">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 {
templ_7745c5c3_Err = absenceComponent(workDay.GetKurzArbeit(), true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
for _, booking := range workDay.Bookings {
templ_7745c5c3_Err = bookingComponent(booking).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " <input type=\"hidden\" name=\"select_kommen\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(len(workDay.Bookings) > 0 && workDay.Bookings[len(workDay.Bookings)-1].CheckInOut%2 == 0)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 174, Col: 140}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
func holidayComponent(d models.IWorkDay) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var24 := templ.GetChildren(ctx)
if templ_7745c5c3_Var24 == nil {
templ_7745c5c3_Var24 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(d.ToString())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 179, Col: 18}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -1,6 +1,7 @@
# cron-timing: 05 01 * * 1 # cron-timing: 05 01 * * 1
container_name="arbeitszeitmessung-main-db-1" container_name="arbeitszeitmessung-main-db-1"
filename=backup-$(date '+%d%m%Y').sql filename=backup-$(date '+%d%m%Y').sql
backup_folder=__BACKUP_FOLDER__
database_name=__DATABASE__ database_name=__DATABASE__
docker exec $container_name pg_dump $database_name > /home/pi/arbeitszeitmessung-backup/$filename docker exec $container_name pg_dump $database_name > $backup_folder/$filename
echo "created backup file: "$filename echo "created backup file: "$filename

View File

@@ -8,6 +8,7 @@ echo "Creating PostgreSQL user and setting permissions... $POSTGRES_USER for API
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE ROLE migrate LOGIN ENCRYPTED PASSWORD '$POSTGRES_PASSWORD'; CREATE ROLE migrate LOGIN ENCRYPTED PASSWORD '$POSTGRES_PASSWORD';
GRANT USAGE, CREATE ON SCHEMA public TO migrate; GRANT USAGE, CREATE ON SCHEMA public TO migrate;
GRANT CONNECT ON DATABASE arbeitszeitmessung TO migrate;
EOSQL EOSQL
# psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL # psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL

View File

@@ -13,9 +13,14 @@ services:
- ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
ports: ports:
- ${POSTGRES_PORT}:5432 - ${POSTGRES_PORT}:5432
healthcheck:
test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}", "--dbname", "${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
backend: backend:
image: git.letsstein.de/tom/arbeitszeitmessung-webserver image: git.letsstein.de/tom/arbeitszeitmessung-webserver:dev
env_file: env_file:
- .env - .env
environment: environment:
@@ -24,12 +29,8 @@ services:
ports: ports:
- ${WEB_PORT}:8080 - ${WEB_PORT}:8080
depends_on: depends_on:
- db db:
condition: service_healthy
volumes: volumes:
- ${LOG_PATH}:/app/logs - ${LOG_PATH}:/app/logs
restart: unless-stopped restart: unless-stopped
# document-creator:
# image: git.letsstein.de/tom/arbeitszeitmessung-doc-creator
# container_name: ${TYPST_CONTAINER}
# restart: unless-stopped

View File

@@ -4,10 +4,10 @@ POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (f
POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung) POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung)
POSTGRES_PATH=__ROOT__/DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...) POSTGRES_PATH=__ROOT__/DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...)
POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name
POSTGRES_PORT=127.0.0.1:5432 # Postgres Port normalerweise nicht freigegeben. regex:^[0-9]{1,5}$ POSTGRES_PORT=5432 # Postgres Port normalerweise nicht freigegeben. regex:^[0-9]{1,5}$
MIGRATIONS_PATH=__ROOT__/migrations # Pfad zu DB migrations (wenn nicht verändert wurde, bei default bleiben)
TZ=Europe/Berlin # Zeitzone TZ=Europe/Berlin # Zeitzone
API_TOKEN=dont_access # API Token für ESP Endpoints API_TOKEN=dont_access # API Token für ESP Endpoints
WEB_PORT=8000 # Port unter welchem Webserver erreichbar ist. regex:^[0-9]{1,5}$ WEB_PORT=8000 # Port unter welchem Webserver erreichbar ist. regex:^[0-9]{1,5}$
LOG_PATH=__ROOT__/logs # Pfad für Audit Logs LOG_PATH=__ROOT__/logs # Pfad für Audit Logs
LOG_LEVEL=warn # Welche Log-Nachrichten werden in der Konsole erscheinen LOG_LEVEL=warn # Welche Log-Nachrichten werden in der Konsole erscheinen
BACKUP_FOLDER=__ROOT__/backup # Pfad für DB Backup Datein

66
Jenkinsfile vendored
View File

@@ -1,66 +0,0 @@
pipeline {
environment {
DOCKER_USERNAME = 'jenkins'
DOCKER_PASSWORD = credentials('gitea_jenkins')
SONAR_TOKEN = credentials('sonarcube_token')
POSTGRES_USER = 'postgres'
POSTGRES_PASSWORD = 'password'
POSTGRES_DB = 'arbeitszeitmessung'
}
agent any
stages {
stage('Test') {
agent {
docker {
image ''
args ''
args ''
}
}
steps {
script {
sh '''
docker run -d --rm \
--name test-db \
-e POSTGRES_USER={$POSTGRES_USER} \
-e POSTGRES_PASSWORD={$POSTGRES_PASSWORD} \
-e POSTGRES_DB={$POSTGRES_DB} \
-v ./DB/initdb:/docker-entrypoint-initdb.d\
-p "5432:5432" \
postgres:16
'''
// docker.image('golang:1.24.5').withRun(
// '-u root:root --network=host'
// ) { go ->
// // wait for DB to start
// sh '''
// cd Backend \
// go mod download && go mod tidy \
// go test ./... -v
// '''
// }
}
}
}
stage('SonarCube Analysis') {
steps {
sh 'make scan'
}
}
stage('Building image arbeitszeit-backend') {
when {
anyOf {
changeset 'Jenkinsfile'
changeset 'Makefile'
changeset 'Backend/**'
}
}
steps {
sh 'make backend'
}
}
}
}

View File

@@ -44,7 +44,7 @@ generateFrontend:
backend: generateFrontend login_registry backend: generateFrontend login_registry
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:latest Backend --push docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung-webserver:dev Backend --push
# docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:${GIT_COMMIT} Backend //--push # docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:${GIT_COMMIT} Backend //--push
test: test:

165
Readme.md
View File

@@ -2,121 +2,76 @@
[![Quality Gate Status](https://sonar.letsstein.de/api/project_badges/measure?project=arbeitszeitmessung&metric=alert_status&token=sqb_253028eff30aff24f32b437cd6c484c511b5c33f)](https://sonar.letsstein.de/dashboard?id=arbeitszeitmessung) [![Quality Gate Status](https://sonar.letsstein.de/api/project_badges/measure?project=arbeitszeitmessung&metric=alert_status&token=sqb_253028eff30aff24f32b437cd6c484c511b5c33f)](https://sonar.letsstein.de/dashboard?id=arbeitszeitmessung)
bis jetzt ein einfaches Backend mit PostgreSQL Datenbank und GO Webserver um Arbeitszeitbuchungen per HTTP PUT einzufügen ---
Eine open-source Software zur Arbeitszeitmessung
## Features
- manuelle Korrektur von einzelnen Buchungen
- Buchung von benutzerdefinierten Abwesenheiten
- automatische gesetzlicher Feiertage
- Pflege eigener Feiertage
- wöchentliches Abrechnungssystem
- Kontrolle der Arbeitszeiten durch direkte Führungskraft
- Ausgabe der Arbeitszeiten je Monat in PDF Format
- Anwesenheitsübersicht
## Installation ## Installation
```bash ```bash
git clone https://git.letsstein.de/tom/arbeitszeitmessung arbeitszeitmessung git clone https://git.letsstein.de/tom/arbeitszeitmessung arbeitszeitmessung
cd arbeitszeitmessung/Docker cd arbeitszeitmessung
# .env Datei anpassen
docker compose up -d ./install.sh
``` ```
## PREVIEW ### Konfiguration:
Zeitverwaltungsansicht (/time): - Datenbank
- `POSTGRES_USER` Postgres ADMIN Nutzername
- `POSTGRES_PASSWORD` Postgres ADMIN Passwort
- `POSTGRES_API_USER` Postgres API Nutzername für Webanwendung
- `POSTGRES_API_PASS` Postgres API Passwort für Webanwendung
- `POSTGRES_PATH` Datebank Pfad
- `POSTGRES_DB` Postgres Datenbank Name
- `POSTGRES_PORT` Postgres Port für administration
- System
- `TZ` Zeitzone
- `LOG_LEVEL` Welche Log-Nachrichten werden in der Konsole erscheinen
- Web/API
- `API_TOKEN` API Token für ESP Endpoints
- `WEB_PORT` Port unter welchem Webserver erreichbar ist
- Ordnerstruktur
- `BACKUP_FOLDER` Pfad für DB Backup Datein
- `LOG_PATH` Pfad für Audit Logs
![time](docs/images/time.png) # Filestrukture
Ansicht der Führungskraft (/team):
![team](docs/images/team.png)
Nutzeransicht (/user):
![user](docs/images/user.png)
## Buchungstypen
1 - Kommen
2 - Gehen
3 - Kommen Manuell
4 - Gehen Manuell
254 - Automatisch abgemeldet
## API
Nutzung der API
wenn die `dev-docker-compose.yml` Datei gestartet wird, ist direkt ein SwaggerUI Server mit entsprechender Datei inbegriffen.
### Buchungen [/time]
#### [GET] Anfrage
Parameter: cardID (string)
Antwort: `200`
```json
[
{
"cradID": "test_card",
"readerID": "test_reader",
"bookingTyp": 2,
"loggedTime": "2024-09-05T08:37:53.117641Z",
"id": 5
},
{
"cradID": "test_card",
"readerID": "mytest",
"bookingTyp": 1,
"loggedTime": "2024-09-05T08:51:12.670827Z",
"id": 6
}
]
``` ```
├── Backend (Webserver)
Antwort `500` │   ├── doc (Templates for Document Creator --> typst used to create PDF Reports)
Serverfehler │   │   ├── static
│   │   └── templates
#### [PUT] Anfrage │   ├── endpoints (HTML Server endpoints (see main.go for Routes))
│   ├── helper (Helper classes)
Parameter: id (int) │   │   ├── logs
Body: (veränderte Parameter) │   │   └── paramParser
│   ├── logs (Log Folder, no sourcecode)
```json │   ├── migrations (DB Migrations Folder, no direct sourcecode)
{ │   ├── models (DB Models and their function)
"cradID": "test_card", │   ├── src (Tailwind src --> used to config css formatter)
"readerID": "mytest", │   ├── static (Webserver static, used to server static content, e.g. JS and CSS files)
"bookingTyp": 1, │   │   └── css
"loggedTime": "2024-09-05T08:51:12.670827Z" │   └── templates (HTML Templates for every page written in templ and compiled to go)
} ├── Cron (all Cron Scripts)
├── DB (local Database mount Point)
│   └── initdb (initialization scripts for DB)
├── Docker (Docker Files, only docker-compose.yaml used)
├── docs
└── └── images
``` ```
Antwort `200`
```json
{
"cradID": "test_card",
"readerID": "mytest",
"bookingTyp": 1,
"loggedTime": "2024-09-05T08:51:12.670827Z",
"id": 6
}
```
### Neue Buchung [/time/new]
#### [PUT] Anfrage
Parameter:
- cardID (string)
- readerID (string)
- bookingType (string)
Antwort `202` Akzeptiert und eingefügt
```json
{
"cradID": "test_card",
"readerID": "mytest",
"bookingTyp": 1,
"loggedTime": "2024-09-05T08:51:12.670827Z",
"id": 6
}
```
Antwort `409` Konflikt
Die vorherige Buchung am selben Tag hat den gleichen Buchungstyp

View File

@@ -64,9 +64,9 @@ if [ ! -f $envFile ]; then
while true; do while true; do
if [ -z "$comment" ]; then if [ -z "$comment" ]; then
printf "Value for $key - $comment (default: $default_value"
else
printf "Value for $key (default: $default_value" printf "Value for $key (default: $default_value"
else
printf "Value for $key - $comment (default: $default_value"
fi fi
if [ -n "$regex" ]; then if [ -n "$regex" ]; then
printf ", must match: %s" "$regex" printf ", must match: %s" "$regex"
@@ -113,28 +113,12 @@ LOG_PATH=$(grep -E '^LOG_PATH=' $envFile | cut -d= -f2)
if [ -z "$LOG_PATH" ]; then if [ -z "$LOG_PATH" ]; then
echo "LOG_PATH not found in .env using default $(pwd)/logs" echo "LOG_PATH not found in .env using default $(pwd)/logs"
LOG_PATH=$(pwd)/logs LOG_PATH=$(pwd)/logs
else
LOG_PATH=Docker/$LOG_PATH
fi fi
mkdir -p $LOG_PATH mkdir -p $LOG_PATH
echo "Created logs folder at $LOG_PATH" echo "Created logs folder at $LOG_PATH"
########################################################################### ###########################################################################
echo -e "\n\n"
echo "Start containers with docker compose up -d? [y/N]"
read -r start_containersmkdi
if [[ "$start_containers" =~ ^[Yy]$ ]]; then
cd Docker
docker compose up -d
echo "Containers started."
else
echo "You can start them manually with: docker compose up -d"
fi
###########################################################################
echo -e "\n\n" echo -e "\n\n"
echo "Setup Crontab for automatic logout, backup and holiday creation? [y/N]" echo "Setup Crontab for automatic logout, backup and holiday creation? [y/N]"
read -r setup_cron read -r setup_cron
@@ -147,13 +131,20 @@ if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
POSTGRES_DB=$(grep -E '^POSTGRES_DB=' $envFile | cut -d= -f2) POSTGRES_DB=$(grep -E '^POSTGRES_DB=' $envFile | cut -d= -f2)
if [ -z "$POSTGRES_DB" ]; then if [ -z "$POSTGRES_DB" ]; then
echo "arbeitszeitmessung not found in .env using default arbeitszeitmessung" echo "POSTGRES_DB not found in .env using default arbeitszeitmessung"
POSTGRES_DB="arbeitszeitmessung" POSTGRES_DB="arbeitszeitmessung"
fi fi
BACKUP_FOLDER=$(grep -E '^BACKUP_FOLDER=' $envFile | cut -d= -f2)
if [ -z "$BACKUP_FOLDER" ]; then
echo "BACKUP_FOLDER not found in .env using default $(pwd)/backup"
BACKUP_FOLDER="$(pwd)/backup"
fi
sed -i "s/__PORT__/$WEB_PORT/" $autoHolidaysScript sed -i "s/__PORT__/$WEB_PORT/" $autoHolidaysScript
sed -i "s/__PORT__/$WEB_PORT/" $autoLogoutScript sed -i "s/__PORT__/$WEB_PORT/" $autoLogoutScript
sed -i "s/__DATABASE__/$POSTGRES_DB/" $autoBackupScript sed -i "s/__DATABASE__/$POSTGRES_DB/" $autoBackupScript
sed -i "s/__BACKUP_FOLDER__/$BACKUP_FOLDER" $autoBackupScript
chmod +x $autoBackupScript $autoHolidaysScript $autoLogoutScript chmod +x $autoBackupScript $autoHolidaysScript $autoLogoutScript
@@ -172,6 +163,7 @@ if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
( crontab -l ; echo "$cron_timing $(pwd)/$file" )| awk '!x[$0]++' | crontab - ( crontab -l ; echo "$cron_timing $(pwd)/$file" )| awk '!x[$0]++' | crontab -
echo "Added entry to crontab: $cron_timing $(pwd)/$file." echo "Added entry to crontab: $cron_timing $(pwd)/$file."
sleep 2
done done
if systemctl is-active --quiet cron.service ; then if systemctl is-active --quiet cron.service ; then
@@ -184,3 +176,18 @@ if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
else else
echo "Please setup cron manually by executing crontab -e and adding all files from inside the Cron directory!" echo "Please setup cron manually by executing crontab -e and adding all files from inside the Cron directory!"
fi fi
###########################################################################
echo -e "\n\n"
echo "Start containers with docker compose up -d? [y/N]"
read -r start_containers
if [[ "$start_containers" =~ ^[Yy]$ ]]; then
cd Docker
docker compose up -d
echo "Containers started."
else
echo "You can start them manually with: docker compose up -d"
fi
echo "Installation finished, you can re-run the script any time!"