36 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
18b2cbc074 Merge pull request 'dev/fix-70' (#71) from dev/fix-70 into main
Some checks failed
Tests / Run Go Tests (push) Failing after 48s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 3m35s
Reviewed-on: #71
2026-01-18 18:11:00 +01:00
560c539b19 fixed minor bugs + added loggin middleware
All checks were successful
Tests / Run Go Tests (push) Successful in 1m26s
2026-01-18 00:07:54 +01:00
502955d32f added migrations back + removed distracting log message 2026-01-17 22:53:35 +01:00
cfd77ae28d fixed #70 + made db script ignore double bookings
Some checks failed
Tests / Run Go Tests (push) Failing after 2m0s
2026-01-17 22:25:25 +01:00
1daf4db167 fixed premission problem after making migrations executed by go
All checks were successful
Tests / Run Go Tests (push) Successful in 1m44s
2026-01-17 21:41:46 +01:00
3322f7e9bc updated install script with cron jobs
All checks were successful
Tests / Run Go Tests (push) Successful in 1m35s
2026-01-07 19:55:00 +01:00
4b9824c714 fixed sonarqube errors
All checks were successful
Tests / Run Go Tests (push) Successful in 2m28s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 3m15s
2026-01-05 12:19:40 +01:00
7ac6c5f9b8 Merge pull request 'dev/pdf' (#69) from dev/pdf into main
Some checks failed
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 4m43s
Tests / Run Go Tests (push) Failing after 3m44s
Reviewed-on: #69
2026-01-05 12:14:46 +01:00
f9fc3d91d1 improved logging + fixed error from log folder
Some checks failed
Tests / Run Go Tests (push) Failing after 3m7s
2026-01-05 12:13:49 +01:00
f0de9961dc removed doc-creator 2026-01-05 12:13:24 +01:00
4ded8632e5 fixed overtime calc issue
Some checks failed
Arbeitszeitmessung Deploy / Build Document Creator (push) Successful in 2m30s
Tests / Run Go Tests (push) Failing after 2m35s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 3m19s
2026-01-05 00:39:00 +01:00
b2af48463c updated git action to build right image
Some checks failed
Arbeitszeitmessung Deploy / Build Document Creator (push) Successful in 2m11s
Tests / Run Go Tests (push) Failing after 2m46s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 3m5s
2026-01-05 00:02:33 +01:00
0b72147e02 Merge pull request 'version 2.0.0 rc' (#68) from dev/feiertage into main
Some checks failed
Tests / Run Go Tests (push) Failing after 2m29s
Arbeitszeitmessung Deploy / Build Document Creator (push) Successful in 4m1s
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 4m4s
Reviewed-on: #68
2026-01-04 23:56:04 +01:00
d1b46cf894 fixed sonarqube errors
Some checks failed
Tests / Run Go Tests (push) Failing after 1m33s
2026-01-04 23:53:16 +01:00
99 changed files with 1786 additions and 4308 deletions

View File

@@ -6,6 +6,7 @@ on:
- "*" - "*"
branches: branches:
- main - main
- dev/main
jobs: jobs:
webserver: webserver:
@@ -39,34 +40,34 @@ jobs:
push: true push: true
context: Backend context: Backend
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
document-creator: # document-creator:
name: Build Document Creator # name: Build Document Creator
runs-on: ubuntu-latest # runs-on: ubuntu-latest
steps: # steps:
- name: Checkout # - name: Checkout
uses: actions/checkout@v4 # uses: actions/checkout@v4
- name: Login to GitHub Container Registry # - name: Login to GitHub Container Registry
uses: docker/login-action@v3 # uses: docker/login-action@v3
with: # with:
registry: git.letsstein.de # registry: git.letsstein.de
username: ${{ gitea.actor }} # username: ${{ gitea.actor }}
password: ${{ secrets.REGISTRY_TOKEN }} # password: ${{ secrets.REGISTRY_TOKEN }}
- name: Set up QEMU # - name: Set up QEMU
uses: docker/setup-qemu-action@v3 # uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx # - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 # uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker # - name: Extract metadata (tags, labels) for Docker
id: meta # id: meta
uses: docker/metadata-action@v5 # uses: docker/metadata-action@v5
with: # with:
images: git.letsstein.de/tom/arbeitszeitmessung-doc-creator # images: git.letsstein.de/tom/arbeitszeitmessung-doc-creator
tags: | # tags: |
type=raw,value=latest # type=raw,value=latest
type=pep440,pattern={{version}} # type=pep440,pattern={{version}}
- name: Build and push # - name: Build and push
uses: docker/build-push-action@v6 # uses: docker/build-push-action@v6
with: # with:
platforms: linux/amd64,linux/arm64 # platforms: linux/amd64,linux/arm64
push: true # push: true
context: Backend # context: DocumentCreator
tags: ${{ steps.meta.outputs.tags }} # tags: ${{ steps.meta.outputs.tags }}

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,22 +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 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 migrations /app/migrations
COPY doc /doc
COPY /static /app/static COPY /static /app/static
ENTRYPOINT ["./server"] ENTRYPOINT ["./server"]

View File

@@ -1,11 +1,19 @@
package main package main
// this file handles the db connection and the
// db migration
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"database/sql" "database/sql"
"fmt" "fmt"
"log/slog"
"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/source/file"
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )
@@ -16,6 +24,45 @@ 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 {
dbHost := helper.GetEnv("POSTGRES_HOST", "localhost")
dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung")
dbPassword := helper.GetEnv("POSTGRES_PASSWORD", "password")
dbTz := helper.GetEnv("TZ", "Europe/Berlin")
connStr := fmt.Sprintf(
"host=%s user=%s dbname=%s password=%s sslmode=disable TimeZone=%s",
dbHost, "migrate", dbName, dbPassword, dbTz)
db, err := sql.Open("postgres", 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 {
return err
}
slog.Info("Connected to database. Running migrations now.")
// Migrate all the way up ...
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return err
}
slog.Info("Finished migrations starting webserver.")
return nil
}

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

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"
@@ -21,7 +24,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
func autoLogout(w http.ResponseWriter) { func autoLogout(w http.ResponseWriter) {
users, err := models.GetAllUsers() users, err := models.GetAllUsers()
var logged_out_users []models.User var loggedOutUsers []models.User
if err != nil { if err != nil {
fmt.Printf("Error getting user list %v\n", err) fmt.Printf("Error getting user list %v\n", err)
} }
@@ -31,14 +34,14 @@ func autoLogout(w http.ResponseWriter) {
if err != nil { if err != nil {
fmt.Printf("Error logging out user %v\n", err) fmt.Printf("Error logging out user %v\n", err)
} else { } else {
logged_out_users = append(logged_out_users, 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)
} }
} }
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(logged_out_users) json.NewEncoder(w).Encode(loggedOutUsers)
} }

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,14 +42,16 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
return return
} }
n := 0 if !helper.IsDebug() {
for _, e := range employes { n := 0
if user.IsSuperior(e) { for _, e := range employes {
employes[n] = e if user.IsSuperior(e) {
n++ employes[n] = e
n++
}
} }
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 {
slog.Error("Error converting days into typst", "error", err)
continue
}
typstDays = append(typstDays, weekTypstDays...)
}
if err != nil { if err != nil {
slog.Warn("Failed to convert to days", slog.Any("error", err)) slog.Error("Cannot retrieve total Overtime", "Error", err)
return typstData{}, 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
@@ -198,8 +227,8 @@ func renderPDFSingle(data []typstData) (bytes.Buffer, error) {
var markup bytes.Buffer var markup bytes.Buffer
var output bytes.Buffer var output bytes.Buffer
typstCLI := typst.DockerExec{ typstCLI := typst.CLI{
ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"), WorkingDirectory: PDF_DIRECTORY,
} }
if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil { if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil {
@@ -225,8 +254,8 @@ func renderPDFSingle(data []typstData) (bytes.Buffer, error) {
func renderPDFMulti(data []typstData) ([]bytes.Buffer, error) { func renderPDFMulti(data []typstData) ([]bytes.Buffer, error) {
var outputMulti []bytes.Buffer var outputMulti []bytes.Buffer
typstRender := typst.DockerExec{ typstRender := typst.CLI{
ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"), WorkingDirectory: PDF_DIRECTORY,
} }
for _, d := range data { for _, d := range data {
@@ -268,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"
@@ -7,6 +10,7 @@ import (
"errors" "errors"
"log" "log"
"net/http" "net/http"
"time"
) )
// Relevant for arduino inputs -> creates new Booking from get and put method // Relevant for arduino inputs -> creates new Booking from get and put method
@@ -36,6 +40,7 @@ func createBooking(w http.ResponseWriter, r *http.Request) {
} }
booking := (*models.Booking).FromUrlParams(nil, r.URL.Query()) booking := (*models.Booking).FromUrlParams(nil, r.URL.Query())
booking.Timestamp = time.Now()
if booking.Verify() { if booking.Verify() {
err := booking.Insert() err := booking.Insert()
if errors.Is(models.SameBookingError{}, err) { if errors.Is(models.SameBookingError{}, err) {

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"
@@ -94,9 +98,9 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
if day.Date().Before(lastSub) { if day.Date().Before(lastSub) {
continue continue
} }
aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, false) 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,21 +162,15 @@ 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
err = newBooking.InsertWithTimestamp() if newBooking.Verify() {
if err != nil { err = newBooking.InsertWithTimestamp()
log.Printf("Error inserting booking %v -> %v\n", newBooking, err) if err != nil {
log.Printf("Error inserting booking %v -> %v\n", newBooking, err)
}
} }
case "change": case "change":
// absenceType, err := strconv.Atoi(r.FormValue("absence"))
// if err != nil {
// log.Println("Error parsing absence type.", err)
// absenceType = 0
// }
// if absenceType != 0 {
// createAbsence(absenceType, user, loc, r)
// }
for index, possibleBooking := range r.PostForm { for index, possibleBooking := range r.PostForm {
if len(index) > 7 && index[:7] == "booking" { if len(index) > 7 && index[:7] == "booking" {
booking_id, err := strconv.Atoi(index[8:]) booking_id, err := strconv.Atoi(index[8:])

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") {
switch r.Method { http.Redirect(w, r, "/time", http.StatusSeeOther)
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)
} }
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

@@ -4,8 +4,6 @@ github.com/Dadido3/go-typst v0.8.0 h1:uTLYprhkrBjwsCXRRuyYUFL0fpYHa2kIYoOB/CGqVN
github.com/Dadido3/go-typst v0.8.0/go.mod h1:QYis9sT70u65kn1SkFfyPRmHsPxgoxWbAixwfPReOZA= github.com/Dadido3/go-typst v0.8.0/go.mod h1:QYis9sT70u65kn1SkFfyPRmHsPxgoxWbAixwfPReOZA=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY=
github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM= github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
@@ -39,6 +37,8 @@ github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -78,7 +78,6 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/wlbr/feiertage v1.10.0/go.mod h1:wJOHvMa6sI5L1FkrTOX/GSoO0hpK3S2YqGLPi8Q84I0=
github.com/wlbr/feiertage v1.17.0 h1:AEck/iUQu19iU0xNEoSQTeSTGXF1Ju0tbAwEi/Lmwqk= github.com/wlbr/feiertage v1.17.0 h1:AEck/iUQu19iU0xNEoSQTeSTGXF1Ju0tbAwEi/Lmwqk=
github.com/wlbr/feiertage v1.17.0/go.mod h1:TVZgmSZgGW/jSxexZ56qdlR6cDj+F/FO8bkw8U6kYxM= github.com/wlbr/feiertage v1.17.0/go.mod h1:TVZgmSZgGW/jSxexZ56qdlR6cDj+F/FO8bkw8U6kYxM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

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,10 +1,13 @@
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"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"context" "context"
"database/sql"
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
@@ -17,7 +20,16 @@ import (
func main() { func main() {
var err error var err error
var logLevel slog.LevelVar var logLevel slog.LevelVar
logLevel.Set(slog.LevelWarn) switch helper.GetEnv("LOG_LEVEL", "warn") {
case "debug":
logLevel.Set(slog.LevelDebug)
case "info":
logLevel.Set(slog.LevelInfo)
case "warn":
logLevel.Set(slog.LevelWarn)
case "error":
logLevel.Set(slog.LevelError)
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &logLevel})) logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &logLevel}))
slog.SetDefault(logger) slog.SetDefault(logger)
@@ -26,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)
@@ -35,6 +47,17 @@ func main() {
models.DB, err = OpenDatabase() models.DB, err = OpenDatabase()
if err != nil { if err != nil {
slog.Error("Error while opening the database", "Error", err) slog.Error("Error while opening the database", "Error", err)
return
}
defer models.DB.(*sql.DB).Close()
if helper.GetEnv("GO_ENV", "production") != "debug" {
err = Migrate()
if err != nil {
slog.Error("Failed to migrate the database to newest version", "Error", err)
return
}
} }
fs := http.FileServer(http.Dir("./static")) fs := http.FileServer(http.Dir("./static"))
@@ -44,33 +67,50 @@ 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))
serverSessionMiddleware := endpoints.Session.LoadAndSave(server) serverSessionMiddleware := endpoints.Session.LoadAndSave(server)
serverSessionMiddleware = loggingMiddleware(serverSessionMiddleware)
// starting the http server // starting the http server
slog.Info("Server is running at http://localhost:8080") slog.Info("Server is running at http://localhost:8080")
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)
slog.Debug("ParamsMiddleware added urlParams", slog.Any("urlParams", queryParams)) if len(queryParams) > 0 {
slog.Debug("ParamsMiddleware added urlParams", slog.Any("urlParams", queryParams))
}
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
}) })
} }
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Log the method and the requested URL
slog.Info("Started request", slog.String("Method", r.Method), slog.String("Path", r.URL.Path))
// Call the next handler in the chain
next.ServeHTTP(w, r)
// Log how long it took
slog.Info("Completet Request", slog.String("Time", time.Since(start).String()))
})
}

View File

@@ -1,3 +1,11 @@
ALTER DEFAULT PRIVILEGES FOR ROLE migrate
IN SCHEMA public
GRANT SELECT ON TABLES TO app_base;
ALTER DEFAULT PRIVILEGES FOR ROLE migrate
IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO app_base;
-- create "abwesenheit" table -- create "abwesenheit" table
CREATE TABLE "abwesenheit" ( CREATE TABLE "abwesenheit" (
"counter_id" bigserial NOT NULL, "counter_id" bigserial NOT NULL,
@@ -6,6 +14,7 @@ CREATE TABLE "abwesenheit" (
"datum" timestamptz NULL DEFAULT (now())::date, "datum" timestamptz NULL DEFAULT (now())::date,
PRIMARY KEY ("counter_id") PRIMARY KEY ("counter_id")
); );
-- create "anwesenheit" table -- create "anwesenheit" table
CREATE TABLE "anwesenheit" ( CREATE TABLE "anwesenheit" (
"counter_id" bigserial NOT NULL, "counter_id" bigserial NOT NULL,
@@ -55,3 +64,6 @@ CREATE TABLE "wochen_report" (
PRIMARY KEY ("id"), PRIMARY KEY ("id"),
CONSTRAINT "wochen_report_personal_nummer_woche_start_key" UNIQUE ("personal_nummer", "woche_start") CONSTRAINT "wochen_report_personal_nummer_woche_start_key" UNIQUE ("personal_nummer", "woche_start")
); );
GRANT INSERT, UPDATE ON abwesenheit, anwesenheit, wochen_report, user_password TO app_base;
GRANT DELETE ON abwesenheit to app_base;

View File

@@ -3,8 +3,3 @@
DROP FUNCTION update_zuletzt_geandert; DROP FUNCTION update_zuletzt_geandert;
DROP TRIGGER IF EXISTS pass_hash_update ON user_password; DROP TRIGGER IF EXISTS pass_hash_update ON user_password;
-- revert: Adds crypto extension
DROP EXTENSION IF EXISTS pgcrypto;

View File

@@ -17,5 +17,3 @@ FOR EACH ROW
EXECUTE FUNCTION update_zuletzt_geandert(); EXECUTE FUNCTION update_zuletzt_geandert();
-- Adds crypto extension -- Adds crypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -11,3 +11,5 @@ CREATE TABLE "s_feiertage" (
); );
-- create index "feiertage_unique_pro_jahr" to table: "s_feiertage" -- create index "feiertage_unique_pro_jahr" to table: "s_feiertage"
CREATE UNIQUE INDEX "feiertage_unique_pro_jahr" ON "s_feiertage" ((EXTRACT(year FROM datum)), "name"); CREATE UNIQUE INDEX "feiertage_unique_pro_jahr" ON "s_feiertage" ((EXTRACT(year FROM datum)), "name");
GRANT INSERT, UPDATE ON s_feiertage TO app_base;

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{}
@@ -128,6 +114,9 @@ func (b *Booking) InsertWithTimestamp() error {
if b.Timestamp.IsZero() { if b.Timestamp.IsZero() {
return b.Insert() return b.Insert()
} }
if !checkLastBooking(*b) {
return SameBookingError{}
}
stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, anwesenheit_typ, timestamp) VALUES ($1, $2, $3, $4, $5) RETURNING counter_id`)) stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, anwesenheit_typ, timestamp) VALUES ($1, $2, $3, $4, $5) RETURNING counter_id`))
if err != nil { if err != nil {
return err return err
@@ -199,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"
@@ -242,12 +231,14 @@ 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
stmt, err := DB.Prepare((`SELECT check_in_out FROM "anwesenheit" WHERE "card_uid" = $1 ORDER BY "timestamp" DESC LIMIT 1;`)) slog.Info("Checking with timestamp:", "timestamp", b.Timestamp.String())
stmt, err := DB.Prepare((`SELECT check_in_out FROM "anwesenheit" WHERE "card_uid" = $1 AND "timestamp" <= $2 ORDER BY "timestamp" DESC LIMIT 1;`))
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).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}
} }
@@ -28,7 +46,7 @@ func (c *CompoundDay) GetWorkDay() WorkDay {
// IsEmpty implements [IWorkDay]. // IsEmpty implements [IWorkDay].
func (c *CompoundDay) IsEmpty() bool { func (c *CompoundDay) IsEmpty() bool {
return len(c.DayParts) > 0 return len(c.DayParts) == 0
} }
// Date implements [IWorkDay]. // Date implements [IWorkDay].
@@ -47,12 +65,16 @@ func (c *CompoundDay) GetDayProgress(u User) int8 {
// GetOvertime implements [IWorkDay]. // GetOvertime implements [IWorkDay].
func (c *CompoundDay) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { func (c *CompoundDay) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
work := c.GetWorktime(u, base, includeKurzarbeit)
var targetHours time.Duration
var overtime time.Duration switch base {
for _, day := range c.DayParts { case WorktimeBaseDay:
overtime += day.GetOvertime(u, base, includeKurzarbeit) targetHours = u.ArbeitszeitProTagFrac(1)
case WorktimeBaseWeek:
targetHours = u.ArbeitszeitProWocheFrac(.2)
} }
return overtime return (work - targetHours).Round(time.Minute)
} }
// GetPausetime implements [IWorkDay]. // GetPausetime implements [IWorkDay].

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,19 +55,18 @@ func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay
} }
for _, absentDay := range absences { for _, absentDay := range absences {
// Kurzarbeit should be integrated in workday if weekDay := absentDay.Date().Weekday(); weekDay == time.Saturday || weekDay == time.Sunday {
existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)]
if !ok {
allDays[absentDay.Date().Format(time.DateOnly)] = &absentDay
continue continue
} }
// Check if there is already a day
existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)]
switch { switch {
case absentDay.AbwesenheitTyp.WorkTime < 0: case absentDay.AbwesenheitTyp.WorkTime < 0:
if workDay, ok := allDays[absentDay.Date().Format(time.DateOnly)].(*WorkDay); ok { if workDay, ok := allDays[absentDay.Date().Format(time.DateOnly)].(*WorkDay); ok {
workDay.kurzArbeit = true workDay.kurzArbeit = true
workDay.kurzArbeitAbsence = absentDay workDay.kurzArbeitAbsence = absentDay
} }
case !existingDay.IsEmpty(): case ok && !existingDay.IsEmpty():
allDays[absentDay.Date().Format(time.DateOnly)] = NewCompondDay(absentDay.Date(), existingDay, &absentDay) allDays[absentDay.Date().Format(time.DateOnly)] = NewCompondDay(absentDay.Date(), existingDay, &absentDay)
default: default:
allDays[absentDay.Date().Format(time.DateOnly)] = &absentDay allDays[absentDay.Date().Format(time.DateOnly)] = &absentDay

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,60 +198,184 @@ 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 (
ordered_bookings AS ( SELECT
SELECT generate_series(
a.timestamp::DATE AS work_date, $2 ::DATE,
a.timestamp, $3 ::DATE - INTERVAL '1 day',
a.check_in_out, INTERVAL '1 day'
a.counter_id, )::DATE AS work_date
a.anwesenheit_typ, ),
sat.anwesenheit_name AS anwesenheit_typ_name, all_bookings AS (
LAG(a.timestamp) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_timestamp, SELECT
LAG(a.check_in_out) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_check a.card_uid,
FROM anwesenheit a a.timestamp,
LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id a.timestamp::DATE AS work_date,
WHERE a.card_uid = $1 a.check_in_out,
AND a.timestamp::DATE >= $2 a.counter_id,
AND a.timestamp::DATE <= $3 a.anwesenheit_typ,
) sat.anwesenheit_name AS anwesenheit_typ_name,
SELECT LAG(a.check_in_out) OVER (
d.work_date, PARTITION BY
COALESCE(MIN(b.timestamp), NOW()) AS time_from, a.card_uid,
COALESCE(MAX(b.timestamp), NOW()) AS time_to, a.timestamp::DATE
COALESCE( ORDER BY
EXTRACT(EPOCH FROM SUM( a.timestamp
CASE ) AS prev_check,
WHEN b.prev_check IN (1, 3) AND b.check_in_out IN (2, 4, 254) LAG(a.timestamp) OVER (
THEN b.timestamp - b.prev_timestamp PARTITION BY
ELSE INTERVAL '0' a.card_uid,
END a.timestamp::DATE
)), 0 ORDER BY
) AS total_work_seconds, a.timestamp
COALESCE( ) AS prev_timestamp
EXTRACT(EPOCH FROM SUM( FROM
CASE anwesenheit a
WHEN b.prev_check IN (2, 4, 254) AND b.check_in_out IN (1, 3) LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id
THEN b.timestamp - b.prev_timestamp WHERE
ELSE INTERVAL '0' a.card_uid = $1
END AND a.timestamp::DATE >= $2::DATE
)), 0 AND a.timestamp::DATE <= $3::DATE
) AS total_pause_seconds, ),
COALESCE(jsonb_agg(jsonb_build_object( normalized_bookings AS (
'check_in_out', b.check_in_out, SELECT
'timestamp', b.timestamp, *
'counter_id', b.counter_id, FROM
'anwesenheit_typ', b.anwesenheit_typ, all_bookings
'anwesenheit_typ', jsonb_build_object( WHERE
'anwesenheit_id', b.anwesenheit_typ, prev_check IS NULL
'anwesenheit_name', b.anwesenheit_typ_name OR prev_check <> check_in_out
) )
) ORDER BY b.timestamp), '[]'::jsonb) AS bookings SELECT
FROM all_days d d.work_date,
LEFT JOIN ordered_bookings b ON d.work_date = b.work_date COALESCE(MIN(b.timestamp), NOW()) AS time_from,
GROUP BY d.work_date COALESCE(MAX(b.timestamp), NOW()) AS time_to,
ORDER BY d.work_date ASC;`) EXTRACT(
EPOCH
FROM
SUM(
CASE
WHEN b.prev_check IN (1, 3)
AND b.check_in_out IN (2, 4, 254) THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0'
END
)
) AS total_work_seconds,
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
)
) 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(`
// WITH all_days AS (
// SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date),
// normalized_bookings AS (
// SELECT *
// FROM (
// SELECT
// a.card_uid,
// a.timestamp,
// a.timestamp::DATE AS work_date,
// a.check_in_out,
// a.counter_id,
// a.anwesenheit_typ,
// sat.anwesenheit_name AS anwesenheit_typ_name,
// LAG(a.check_in_out) OVER (
// PARTITION BY a.card_uid, a.timestamp::DATE
// ORDER BY a.timestamp
// ) AS prev_check
// 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
// 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
// d.work_date,
// COALESCE(MIN(b.timestamp), NOW()) AS time_from,
// COALESCE(MAX(b.timestamp), NOW()) AS time_to,
// COALESCE(
// EXTRACT(EPOCH FROM SUM(
// CASE
// WHEN b.prev_check IN (1, 3) AND b.check_in_out IN (2, 4, 254)
// THEN b.timestamp - b.prev_timestamp
// ELSE INTERVAL '0'
// 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
// FROM all_days d
// LEFT JOIN ordered_bookings b ON d.work_date = b.work_date
// GROUP BY d.work_date
// ORDER BY d.work_date ASC;`)
if err != nil { if err != nil {
log.Println("Error preparing SQL statement", err) log.Println("Error preparing SQL statement", err)
@@ -244,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))
err = json.Unmarshal(bookings, &workDay.Bookings) if bookings != nil {
if err != nil { err = json.Unmarshal(bookings, &workDay.Bookings)
log.Println("Error parsing bookings JSON!", err) if err != nil {
return nil slog.Error("Error parsing bookings JSON!", "Error", err, "Json", bookings)
return nil
}
} }
// better empty day handling // better empty day handling
if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 { // if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 {
workDay.Bookings = []Booking{} // 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
@@ -285,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

@@ -1,163 +1,157 @@
@import "tailwindcss"; @import "tailwindcss";
@source "../templates/*.templ"; @source "../templates/*.templ";
@plugin "@iconify/tailwind4" { @plugin "@iconify/tailwind4" {
scale: 1.25; scale: 1.25;
} }
@theme { @theme {
--color-accent-50: #e7fdea; --color-accent-50: #e7fdea;
--color-accent-100: #cbfbd1; --color-accent-100: #cbfbd1;
--color-accent-200: #9cf7a8; --color-accent-200: #9cf7a8;
--color-accent-300: #68f37a; --color-accent-300: #68f37a;
--color-accent-400: #33ef4d; --color-accent-400: #33ef4d;
--color-accent-500: #11db2d; --color-accent-500: #11db2d;
--color-accent-600: #0eaf23; --color-accent-600: #0eaf23;
--color-accent-700: #0a851b; --color-accent-700: #0a851b;
--color-accent-800: #075a12; --color-accent-800: #075a12;
--color-accent-900: #032b09; --color-accent-900: #032b09;
--color-accent-950: #021805; --color-accent-950: #021805;
--color-accent: #0eaf23; --color-accent: #0eaf23;
--color-text-50: #f7f8f7; --color-text-50: #f7f8f7;
--color-text-100: #f2f3f2; --color-text-100: #f2f3f2;
--color-text-200: #e2e4e2; --color-text-200: #e2e4e2;
--color-text-300: #d2d6d2; --color-text-300: #d2d6d2;
--color-text-400: #c2c7c2; --color-text-400: #c2c7c2;
--color-text-500: #afb6af; --color-text-500: #afb6af;
--color-text-600: #97a097; --color-text-600: #97a097;
--color-text-700: #7d877d; --color-text-700: #7d877d;
--color-text-800: #5a625a; --color-text-800: #5a625a;
--color-text-900: #161816; --color-text-900: #161816;
--color-text-950: #000000; --color-text-950: #000000;
} }
@layer base { @layer base {
body { body {
-webkit-print-color-adjust: exact !important; -webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important; print-color-adjust: exact !important;
background-color: white; background-color: white;
} }
} }
@layer components { @layer components {
.grid-main {
display: grid;
grid-template-columns: 4fr 3fr 3fr 1fr;
align-items: stretch;
}
.grid-sub {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
border-color: var(--color-neutral-400);
transition: background-color 0.2s ease-in-out;
}
.grid-sub.responsive {
display: flex;
flex-direction: column;
}
.grid-sub:hover {
background-color: var(--color-neutral-200);
}
.grid-cell {
padding: calc(var(--spacing) * 2);
border-color: var(--color-neutral-400);
}
.btn {
width: 100%;
cursor: pointer;
border-radius: var(--radius-md);
color: var(--color-neutral-800);
font-size: var(--text-sm);
text-align: center;
padding: calc(var(--spacing) * 2);
border-style: var(--tw-border-style);
border-width: 1px;
border-color: var(--color-neutral-800);
transition-property:
color, background-color, border-color, outline-color,
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));
}
input.btn,
select.btn {
transition-duration: 300ms;
}
.btn:hover {
color: var(--color-white);
background-color: var(--color-neutral-700);
}
.btn:disabled {
opacity: 50%;
pointer-events: none;
}
input.btn,
select.btn {
text-align: left;
}
input.btn:hover,
select.btn:hover {
border-color: var(--color-neutral-300);
background-color: var(--color-neutral-100);
color: var(--color-neutral-800);
}
.edit-box {
border-radius: var(--radius-md);
overflow: hidden;
border-color: var(--color-neutral-500);
transition-property: background-color, border-color;
transition-timing-function: var(--default-transition-timing-function) * 2;
transition-duration: var(--default-transition-duration);
outline: none;
&:is(:where(.group):is(.edit) *) {
border-width: 1px;
}
}
.edit-box:hover {
&:is(:where(.group):is(.edit) *) {
background-color: var(--color-white);
border-color: var(--color-neutral-300);
}
}
.edit-box input:focus {
outline: none;
}
div.edit {
border-width: 1px;
background-color: var(--color-neutral-300);
}
@media (width >=48rem) {
.grid-main { .grid-main {
grid-template-columns: repeat(5, 1fr); display: grid;
margin: 0 10%; grid-template-columns: 4fr 3fr 3fr 1fr;
align-items: stretch;
}
.grid-sub {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
border-color: var(--color-neutral-400);
transition: background-color 0.2s ease-in-out;
} }
.grid-sub.responsive { .grid-sub.responsive {
display: grid; display: flex;
flex-direction: column;
}
.grid-sub:hover {
background-color: var(--color-neutral-200);
}
.grid-cell {
padding: calc(var(--spacing) * 2);
border-color: var(--color-neutral-400);
} }
.btn { .btn {
padding-inline: calc(var(--spacing) * 4); width: 100%;
cursor: pointer;
border-radius: var(--radius-md);
color: var(--color-neutral-800);
font-size: var(--text-sm);
text-align: center;
padding: calc(var(--spacing) * 2);
border-style: var(--tw-border-style);
border-width: 1px;
border-color: var(--color-neutral-800);
transition-property: color, background-color, border-color, outline-color, 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));
}
input.btn,
select.btn {
transition-duration: 300ms;
}
.btn:hover {
color: var(--color-white);
background-color: var(--color-neutral-700);
}
.btn:disabled {
opacity: 50%;
pointer-events: none;
}
input.btn,
select.btn {
text-align: left;
}
input.btn:hover,
select.btn:hover {
border-color: var(--color-neutral-300);
background-color: var(--color-neutral-100);
color: var(--color-neutral-800);
}
.edit-box {
border-radius: var(--radius-md);
overflow: hidden;
border-color: var(--color-neutral-500);
transition-property: background-color, border-color;
transition-timing-function: var(--default-transition-timing-function) * 2;
transition-duration: var(--default-transition-duration);
outline: none;
&:is(:where(.group):is(.edit) *) {
border-width: 1px;
}
}
.edit-box:hover {
&:is(:where(.group):is(.edit) *) {
background-color: var(--color-white);
border-color: var(--color-neutral-300);
}
}
.edit-box input:focus {
outline: none;
}
div.edit {
border-width: 1px;
background-color: var(--color-neutral-300);
}
@media (width >=48rem) {
.grid-main {
grid-template-columns: repeat(5, 1fr);
margin: 0 10%;
}
.grid-sub.responsive {
display: grid;
}
.btn {
padding-inline: calc(var(--spacing) * 4);
}
} }
}
} }

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) {
@@ -1081,7 +1004,7 @@
border-width: 1px; border-width: 1px;
border-color: var(--color-neutral-800); border-color: var(--color-neutral-800);
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; transition-property: color, background-color, border-color, outline-color, 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-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));
} }
input.btn, select.btn { input.btn, select.btn {
@@ -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

@@ -94,8 +94,8 @@ function checkAll(pattern, state) {
} }
} }
bookingForms = document.querySelectorAll("form.bookings"); const bookingForms = document.querySelectorAll("form.bookings");
for (form of bookingForms) { for (let form of bookingForms) {
let selectKommenInput = form.querySelector("input[name='select_kommen']"); let selectKommenInput = form.querySelector("input[name='select_kommen']");
let kommenGehenSelector = form.querySelector("select"); let kommenGehenSelector = form.querySelector("select");
if (selectKommenInput) { if (selectKommenInput) {

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,62 +2,25 @@ 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>
<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) }>
<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>
<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 { } else {
<div class={ "w-2 h-full bg-accent rounded-md flex-shrink-0", bgColor }></div> <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 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>
<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>
} }
} }
@@ -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() }
if !booking.Valid {
fehlerhafte Buchung, wird nicht zur Berechnung verwendet!
}
</p> </p>
if booking.IsSubmittedAndChecked() {
<p>submitted</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() {
@@ -115,7 +119,7 @@ templ defaultDayComponent(day models.IWorkDay) {
if pause > 0 { if pause > 0 {
<p class="text-neutral-500 flex flex-row items-center"><span class="icon-[material-symbols-light--motion-photos-paused-outline]"></span>{ helper.FormatDuration(pause) }</p> <p class="text-neutral-500 flex flex-row items-center"><span class="icon-[material-symbols-light--motion-photos-paused-outline]"></span>{ helper.FormatDuration(pause) }</p>
} }
if overtime != 0 && day.IsEmpty() == false { if !day.IsEmpty() && overtime != 0 {
<p class="text-neutral-500 flex flex-row items-center"> <p class="text-neutral-500 flex flex-row items-center">
<span class="icon-[material-symbols-light--more-time]"></span> <span class="icon-[material-symbols-light--more-time]"></span>
{ helper.FormatDuration(overtime) } { helper.FormatDuration(overtime) }
@@ -138,8 +142,11 @@ templ defaultDayComponent(day models.IWorkDay) {
<input type="hidden" name="action" value="change"/> <!-- default action value for ändern button --> <input type="hidden" name="action" value="change"/> <!-- default action value for ändern button -->
</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 overtime != 0 && day.IsEmpty() == false {
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

7
Cron/autoBackup.sh Executable file
View File

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

3
Cron/autoHolidays.sh Executable file
View File

@@ -0,0 +1,3 @@
# Calls endpoint to write all public Holidays for the current year inside a database.
port=__PORT__
curl localhost:$port/auto/feiertage

4
Cron/autoLogout.sh Executable file
View File

@@ -0,0 +1,4 @@
# cron-timing: 55 23 * * *
# Calls endpoint to log out all users, still logged in for today
port=__PORT__
curl localhost:$port/auto/logout

56
DB/initdb/01_create_user.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
set -e # Exit on error
echo "Creating PostgreSQL user and setting permissions... $POSTGRES_USER for API user $POSTGRES_API_USER"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE ROLE migrate LOGIN ENCRYPTED PASSWORD '$POSTGRES_PASSWORD';
GRANT USAGE, CREATE ON SCHEMA public TO migrate;
GRANT CONNECT ON DATABASE arbeitszeitmessung TO migrate;
EOSQL
# psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
# GRANT SELECT, INSERT, UPDATE ON anwesenheit, abwesenheit, user_password, wochen_report, s_feiertage TO $POSTGRES_API_USER;
# GRANT DELETE ON abwesenheit TO $POSTGRES_API_USER;
# GRANT SELECT ON s_personal_daten, s_abwesenheit_typen, s_anwesenheit_typen, s_feiertage TO $POSTGRES_API_USER;
# GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO $POSTGRES_API_USER;
# EOSQL
echo "User creation and permissions setup complete!"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
-- privilege roles
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_base') THEN
CREATE ROLE app_base NOLOGIN;
END IF;
END
\$\$;
-- dynamic login role
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_API_USER') THEN
CREATE ROLE $POSTGRES_API_USER
LOGIN
ENCRYPTED PASSWORD '$POSTGRES_API_PASS';
END IF;
END
\$\$;
-- grant base privileges
GRANT app_base TO $POSTGRES_API_USER;
GRANT CONNECT ON DATABASE $POSTGRES_DB TO $POSTGRES_API_USER;
GRANT USAGE ON SCHEMA public TO $POSTGRES_API_USER;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
EOSQL
# psql -v ON_ERROR_STOP=1 --username root --dbname arbeitszeitmessung

View File

@@ -1,8 +0,0 @@
INSERT INTO "s_personal_daten" ("personal_nummer", "aktiv_beschaeftigt", "vorname", "nachname", "geburtsdatum", "plz", "adresse", "geschlecht", "card_uid", "hauptbeschaeftigungs_ort", "arbeitszeit_per_tag", "arbeitszeit_per_woche", "arbeitszeit_min_start", "arbeitszeit_max_ende", "vorgesetzter_pers_nr") VALUES
(123, 't', 'Kim', 'Mustermensch', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'aaaa-aaaa', 1, 8, 40, '07:00:00', '20:00:00', 0);
INSERT INTO "user_password" ("personal_nummer", "pass_hash") VALUES
(123, crypt('max_pass', gen_salt('bf')));
INSERT INTO "s_anwesenheit_typen" ("anwesenheit_id", "anwesenheit_name") VALUES (1, 'Büro');
INSERT INTO "s_abwesenheit_typen" ("abwesenheit_id", "abwesenheit_name", "arbeitszeit_equivalent") VALUES (1, 'Urlaub', 100), (2, 'Krank', 100), (3, 'Kurzarbeit', -1), (4, 'Urlaub untertags', 50);

View File

@@ -1,21 +0,0 @@
#!/bin/bash
set -e # Exit on error
echo "Creating PostgreSQL user and setting permissions... $POSTGRES_USER for API user $POSTGRES_API_USER"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER $POSTGRES_API_USER WITH ENCRYPTED PASSWORD '$POSTGRES_API_PASS';
EOSQL
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
GRANT CONNECT ON DATABASE $POSTGRES_DB TO $POSTGRES_API_USER;
GRANT USAGE ON SCHEMA public TO $POSTGRES_API_USER;
GRANT SELECT, INSERT, UPDATE ON anwesenheit, abwesenheit, user_password, wochen_report, s_feiertage TO $POSTGRES_API_USER;
GRANT DELETE ON abwesenheit TO $POSTGRES_API_USER;
GRANT SELECT ON s_personal_daten, s_abwesenheit_typen, s_anwesenheit_typen, s_feiertage TO $POSTGRES_API_USER;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO $POSTGRES_API_USER;
EOSQL
echo "User creation and permissions setup complete!"
# psql -v ON_ERROR_STOP=1 --username root --dbname arbeitszeitmessung

View File

@@ -3,7 +3,7 @@ services:
db: db:
volumes: volumes:
- ${POSTGRES_PATH}:/var/lib/postgresql/data - ${POSTGRES_PATH}:/var/lib/postgresql/data
# - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
ports: ports:
- 5432:5432 - 5432:5432

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,13 +29,8 @@ services:
ports: ports:
- ${WEB_PORT}:8080 - ${WEB_PORT}:8080
depends_on: depends_on:
- db db:
- document-creator condition: service_healthy
volumes: volumes:
- ../logs:/app/Backend/logs - ${LOG_PATH}:/app/logs
restart: unless-stopped
document-creator:
image: git.letsstein.de/tom/arbeitszeitmessung-doc-creator
container_name: ${TYPST_CONTAINER}
restart: unless-stopped restart: unless-stopped

View File

@@ -2,11 +2,12 @@ POSTGRES_USER=root # Postgres ADMIN Nutzername
POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort
POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung) POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung)
POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung) POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung)
POSTGRES_PATH=../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 /...)
LOG_PATH=../logs # Pfad für Logdatein
POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name
POSTGRES_PORT=127.0.0.1:5432 # Postgres Port will not be exposed by default. regex:^[0-9]{1,5}$ POSTGRES_PORT=5432 # Postgres Port normalerweise nicht freigegeben. regex:^[0-9]{1,5}$
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 from which Arbeitszeitmessung should be accessable regex:^[0-9]{1,5}$ WEB_PORT=8000 # Port unter welchem Webserver erreichbar ist. regex:^[0-9]{1,5}$
TYPST_CONTAINER=arbeitszeitmessung-doc-creator # Name of the pdf compiler container LOG_PATH=__ROOT__/logs # Pfad für Audit Logs
LOG_LEVEL=warn # Welche Log-Nachrichten werden in der Konsole erscheinen
BACKUP_FOLDER=__ROOT__/backup # Pfad für DB Backup Datein

View File

@@ -1,7 +0,0 @@
FROM ghcr.io/typst/typst:0.14.0
WORKDIR /app
COPY ./templates /app/templates
COPY ./static /app/static
ENTRYPOINT ["sh", "-c", "while true; do sleep 3600; done"]

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

@@ -4,6 +4,10 @@ set -e
envFile=Docker/.env envFile=Docker/.env
envExample=Docker/env.example envExample=Docker/env.example
autoBackupScript=Cron/autoBackup.sh
autoHolidaysScript=Cron/autoHolidays.sh
autoLogoutScript=Cron/autoLogout.sh
echo "Checking Docker installation..." echo "Checking Docker installation..."
if ! command -v docker >/dev/null 2>&1; then if ! command -v docker >/dev/null 2>&1; then
echo "Docker not found. Install Docker? [y/N]" echo "Docker not found. Install Docker? [y/N]"
@@ -18,12 +22,16 @@ else
echo "Docker is already installed." echo "Docker is already installed."
fi fi
###########################################################################
echo "Checking Docker Compose..." echo "Checking Docker Compose..."
if ! docker compose version >/dev/null 2>&1; then if ! docker compose version >/dev/null 2>&1; then
echo "Docker Compose plugin missing. You may need to update Docker." echo "Docker Compose plugin missing. You may need to update Docker."
exit 1 exit 1
fi fi
###########################################################################
echo "Preparing .env file..." echo "Preparing .env file..."
if [ ! -f $envFile ]; then if [ ! -f $envFile ]; then
if [ -f $envExample ]; then if [ -f $envExample ]; then
@@ -44,6 +52,9 @@ if [ ! -f $envFile ]; then
raw_val=$(printf "%s" "$rest" | sed 's/ *#.*//') raw_val=$(printf "%s" "$rest" | sed 's/ *#.*//')
default_value=$(printf "%s" "$raw_val" | sed 's/"//g') default_value=$(printf "%s" "$raw_val" | sed 's/"//g')
# Replace __ROOT__ with script pwd
default_value="${default_value/__ROOT__/$(pwd)}"
regex="" regex=""
if [[ "$comment" =~ regex:(.*)$ ]]; then if [[ "$comment" =~ regex:(.*)$ ]]; then
regex="${BASH_REMATCH[1]}" regex="${BASH_REMATCH[1]}"
@@ -53,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"
@@ -96,13 +107,87 @@ else
echo "Using existing .env. (found at $envFile)" echo "Using existing .env. (found at $envFile)"
fi fi
###########################################################################
LOG_PATH=$(grep -E '^LOG_PATH=' $envFile | cut -d= -f2)
if [ -z "$LOG_PATH" ]; then
echo "LOG_PATH not found in .env using default $(pwd)/logs"
LOG_PATH=$(pwd)/logs
fi
mkdir -p $LOG_PATH
echo "Created logs folder at $LOG_PATH"
###########################################################################
echo -e "\n\n"
echo "Setup Crontab for automatic logout, backup and holiday creation? [y/N]"
read -r setup_cron
if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
WEB_PORT=$(grep -E '^WEB_PORT=' $envFile | cut -d= -f2)
if [ -z "$WEB_PORT" ]; then
echo "WEB_PORT not found in .env using default 8000"
WEB_PORT=8000
fi
POSTGRES_DB=$(grep -E '^POSTGRES_DB=' $envFile | cut -d= -f2)
if [ -z "$POSTGRES_DB" ]; then
echo "POSTGRES_DB not found in .env using default arbeitszeitmessung"
POSTGRES_DB="arbeitszeitmessung"
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/" $autoLogoutScript
sed -i "s/__DATABASE__/$POSTGRES_DB/" $autoBackupScript
sed -i "s/__BACKUP_FOLDER__/$BACKUP_FOLDER" $autoBackupScript
chmod +x $autoBackupScript $autoHolidaysScript $autoLogoutScript
# echo "Scripts build with PORT=$WEB_PORT and DATABSE=$POSTGRES_DB!"
echo "Adding rules to crontab."
cron_commands=$(mktemp /tmp/arbeitszeitmessung-cron.XXX)
for file in Cron/*; do
cron_timing=$(grep -E '^# cron-timing:' "$file" | sed 's/^# cron-timing:[[:space:]]*//')
if [ -z "$cron_timing" ]; then
echo "No cron-timing found in $file, so it's not added to crontab."
continue
fi
( crontab -l ; echo "$cron_timing $(pwd)/$file" )| awk '!x[$0]++' | crontab -
echo "Added entry to crontab: $cron_timing $(pwd)/$file."
sleep 2
done
if systemctl is-active --quiet cron.service ; then
echo "cron.service is running. Everything should be fine now."
else
echo "cron.service is not running. Please start and enable cron.service."
echo "For how to start a service, see: https://wiki.ubuntuusers.de/systemd/systemctl UNITNAME will be cron.service"
fi
else
echo "Please setup cron manually by executing crontab -e and adding all files from inside the Cron directory!"
fi
###########################################################################
echo -e "\n\n"
echo "Start containers with docker compose up -d? [y/N]" echo "Start containers with docker compose up -d? [y/N]"
read -r start_containers read -r start_containers
if [[ "$start_containers" =~ ^[Yy]$ ]]; then if [[ "$start_containers" =~ ^[Yy]$ ]]; then
cd Docker cd Docker
mkdir ../logs
docker compose up -d docker compose up -d
echo "Containers started." echo "Containers started."
else else
echo "You can start them manually with: docker compose up -d" echo "You can start them manually with: docker compose up -d"
fi fi
echo "Installation finished, you can re-run the script any time!"