30 Commits

Author SHA1 Message Date
085287c7a5 Merge pull request 'dev/finalFixes' (#81) from dev/finalFixes into main
All checks were successful
Arbeitszeitmessung Deploy / Build Webserver (push) Successful in 8m45s
Tests / Run Go Tests (push) Successful in 8m48s
Reviewed-on: #81
2026-03-01 10:11:45 +01:00
c29a952e1d fix: changed backup filename for better sorting of backups
All checks were successful
Tests / Run Go Tests (push) Successful in 1m43s
2026-02-27 16:18:28 +01:00
b12a467ef9 updated install script to also reconfigure/update everything 2026-02-26 21:43:40 +01:00
8bb1777519 feat: booking can only in between specified hours
every booking happening outside these hours will be clamped to the hours
also added few more config options + regex filters
2026-02-25 01:02:15 +01:00
f21ce9a3c3 fixed: pause time calculation from work instead of presence time
All checks were successful
Tests / Run Go Tests (push) Successful in 1m44s
2026-02-24 20:05:08 +01:00
b4bf550863 fix: closing some issues from sonarqube
Some checks failed
Tests / Run Go Tests (push) Failing after 59s
2026-02-15 18:49:29 +01:00
10df10a606 fix: calc worktime, when nothing is set
Some checks failed
Tests / Run Go Tests (push) Failing after 1m32s
2026-02-15 18:30:28 +01:00
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
91 changed files with 2289 additions and 4488 deletions

View File

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

View File

@@ -46,6 +46,8 @@ jobs:
key: arbeitszeitmessung-${{ steps.hash-go.outputs.hash }}
restore-keys: |-
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
run: cd Backend && mkdir .test && go test ./... -coverprofile=.test/coverage.out -json > .test/report.json
- name: Verify coverage report exists

5
.gitignore vendored
View File

@@ -30,7 +30,8 @@ DB/pg_data
.env.*
.env
!.env.example
Docker/config
.idea
.vscode
@@ -40,3 +41,5 @@ atlas.hcl
.scannerwork
Backend/logs
.worktime.txt
*_templ.go

View File

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

View File

@@ -1,24 +1,34 @@
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:alpine AS build
FROM --platform=$BUILDPLATFORM golang:alpine AS base
ARG TARGETOS
ARG TARGETARCH
ENV CGO_ENABLED=0 \
GOOS=$TARGETOS \
GOARCH=$TARGETARCH
FROM base AS fetch-stage
WORKDIR /app
COPY go.mod go.sum /app/
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 .
FROM alpine:3.22
RUN apk add --no-cache tzdata typst
WORKDIR /app
COPY --from=build /app/server /app/server
COPY ./doc/static /doc/static
COPY ./doc/templates /doc/templates
COPY migrations /app/migrations
COPY doc /doc
COPY /static /app/static
ENTRYPOINT ["./server"]

View File

@@ -1,5 +1,8 @@
package main
// this file handles the db connection and the
// db migration
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
@@ -8,6 +11,7 @@ import (
"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"
@@ -20,21 +24,34 @@ func OpenDatabase() (models.IDatabase, error) {
dbPassword := helper.GetEnv("POSTGRES_API_PASS", "password")
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)
}
func Migrate() error {
dbHost := helper.GetEnv("POSTGRES_HOST", "localhost")
dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung")
// dbUser := helper.GetEnv("POSTGRES_USER", "api_nutzer")
dbPassword := helper.GetEnv("POSTGRES_PASSWORD", "password")
dbTz := helper.GetEnv("TZ", "Europe/Berlin")
migrations := helper.GetEnv("MIGRATIONS_PATH", "../migrations")
connStr := fmt.Sprintf(
"host=%s user=%s dbname=%s password=%s sslmode=disable TimeZone=%s",
dbHost, "migrate", dbName, dbPassword, dbTz)
connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=%s", "migrate", dbPassword, dbHost, dbName, dbTz)
m, err := migrate.New(fmt.Sprintf("file://%s", migrations), connStr)
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
}

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
// 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 (
"arbeitszeitmessung/helper/paramParser"
"arbeitszeitmessung/models"

View File

@@ -1,5 +1,10 @@
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 (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser"
@@ -58,8 +63,8 @@ func fillKurzarbeit(r *http.Request, w http.ResponseWriter) {
workday, _ := day.(*models.WorkDay)
lastBookingTime := workday.Bookings[len(workday.Bookings)-1].Timestamp
kurzarbeitBegin := (*models.Booking).New(nil, user.CardUID, 0, 1, bookingTypeKurzarbeit.Id)
kurzarbeitEnd := (*models.Booking).New(nil, user.CardUID, 0, 2, bookingTypeKurzarbeit.Id)
kurzarbeitBegin := (*models.Booking).NewBooking(nil, user.CardUID, 0, 1, bookingTypeKurzarbeit.Id)
kurzarbeitEnd := (*models.Booking).NewBooking(nil, user.CardUID, 0, 2, bookingTypeKurzarbeit.Id)
kurzarbeitBegin.Timestamp = lastBookingTime.Add(time.Minute)
kurzarbeitEnd.Timestamp = lastBookingTime.Add(worktimeKurzarbeit)

View File

@@ -1,5 +1,8 @@
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 (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
@@ -32,7 +35,7 @@ func autoLogout(w http.ResponseWriter) {
fmt.Printf("Error logging out user %v\n", err)
} else {
loggedOutUsers = append(loggedOutUsers, user)
log.Printf("Automaticaly logged out user %s, %s ", user.Name, user.Vorname)
log.Printf("Automaticaly logged out user %d ", user.PersonalNummer)
}
}

View File

@@ -1,5 +1,8 @@
package endpoints
// this endpoint served at "/pdf/create" accepts the contents from the pdf form
// and renders a pdf according to this form
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser"
@@ -18,66 +21,7 @@ import (
const DE_DATE string = "02.01.2006"
const FILE_YEAR_MONTH string = "2006_01"
func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) {
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
}
var PDF_DIRECTORY = helper.GetEnv("PDF_PATH", "/doc/")
func PDFCreateController(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r)
@@ -98,6 +42,7 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
return
}
if !helper.IsDebug() {
n := 0
for _, e := range employes {
if user.IsSuperior(e) {
@@ -106,6 +51,7 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
}
}
employes = employes[:n]
}
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))
w.WriteHeader(http.StatusInternalServerError)
}
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)
w.WriteHeader(http.StatusOK)
case "download":
@@ -128,11 +75,12 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
}
output, err := zipPfd(pdfReports, &reportData)
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.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)
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 {
startDate = helper.GetFirstOfMonth(startDate)
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) {
publicHolidays, err := models.GetHolidaysFromTo(startDate, endDate)
targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)-len(publicHolidays))
workDaysThisMonth := models.GetDays(employee, startDate, endDate.AddDate(0, 0, 1), false)
slog.Debug("Baseline Working hours", "targetHours", targetHoursThisMonth.Hours())
// publicHolidays, _ := models.GetHolidaysFromTo(startDate, endDate)
targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)) //-len(publicHolidays)
mondaysThisMonth := helper.GetMondays(helper.GenerateDateRange(startDate, endDate), false)
var weeks []models.WorkWeek
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.Warn("Failed to convert to days", slog.Any("error", err))
return typstData{}, err
slog.Error("Error converting days into typst", "error", err)
continue
}
typstDays = append(typstDays, weekTypstDays...)
}
if err != nil {
slog.Error("Cannot retrieve total Overtime", "Error", err)
}
metadata := typstMetadata{
EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name),
TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)),
Overtime: helper.FormatDurationFill(worktimeBalance, true),
Overtime: helper.FormatDurationFill(monthOvertime, true),
WorkTime: helper.FormatDurationFill(workHours, true),
Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true),
OvertimeTotal: "",
OvertimeTotal: helper.FormatDurationFill(totalOvertime+monthOvertime, true),
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
@@ -199,8 +228,7 @@ func renderPDFSingle(data []typstData) (bytes.Buffer, error) {
var output bytes.Buffer
typstCLI := typst.CLI{
WorkingDirectory: "/doc/",
// ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
WorkingDirectory: PDF_DIRECTORY,
}
if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil {
@@ -227,8 +255,7 @@ func renderPDFMulti(data []typstData) ([]bytes.Buffer, error) {
var outputMulti []bytes.Buffer
typstRender := typst.CLI{
WorkingDirectory: "/doc/",
// ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
WorkingDirectory: PDF_DIRECTORY,
}
for _, d := range data {
@@ -270,6 +297,16 @@ func zipPfd(pdfReports []bytes.Buffer, reportData *[]typstData) (bytes.Buffer, e
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 {
TimeRange string `json:"time-range"`
EmployeeName string `json:"employee-name"`

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
package endpoints
// this endpoint served at "/team/report" handles the report page
// and also the submission/change of reports
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser"
@@ -12,7 +15,7 @@ import (
"time"
)
func TeamHandler(w http.ResponseWriter, r *http.Request) {
func ReportHandler(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r)
switch r.Method {
case http.MethodPost:
@@ -39,7 +42,7 @@ func submitReport(w http.ResponseWriter, r *http.Request) {
return
}
workWeek := models.NewWorkWeek(user, weekTs, true)
workWeek := models.NewWorkWeekSimple(user, weekTs, true)
switch r.FormValue("method") {
case "send":
@@ -67,7 +70,7 @@ func showWeeks(w http.ResponseWriter, r *http.Request) {
submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission())
lastSub := helper.GetMonday(submissionDate)
userWeek := models.NewWorkWeek(user, lastSub, true)
userWeek := models.NewWorkWeekSimple(user, lastSub, true)
var workWeeks []models.WorkWeek
teamMembers, err := user.GetTeamMembers()
@@ -76,5 +79,5 @@ func showWeeks(w http.ResponseWriter, r *http.Request) {
workWeeks = append(workWeeks, weeks...)
}
// 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
// this endpoint served at "/time/create" is for the esp api and creates bookings
// either via HTTP GET or HTTP PUT
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
@@ -7,7 +10,6 @@ import (
"errors"
"log"
"net/http"
"time"
)
// Relevant for arduino inputs -> creates new Booking from get and put method
@@ -37,7 +39,7 @@ func createBooking(w http.ResponseWriter, r *http.Request) {
}
booking := (*models.Booking).FromUrlParams(nil, r.URL.Query())
booking.Timestamp = time.Now()
// booking.Timestamp = time.Now()
if booking.Verify() {
err := booking.Insert()
if errors.Is(models.SameBookingError{}, err) {

View File

@@ -1,5 +1,9 @@
package endpoints
// this endpoint served at "/time" handles the time page
// this includes normal show + creation of bookings from the webpage +
// edit functionality
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser"
@@ -96,7 +100,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
}
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)
} else {
log.Println("Cannot calculate overtime: ", err)
@@ -158,7 +162,7 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
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
if newBooking.Verify() {
err = newBooking.InsertWithTimestamp()
@@ -253,12 +257,6 @@ func updateAbsence(r *http.Request) error {
log.Println("Cannot get Absence for id: ", absenceId, err)
return err
}
if r.FormValue("action") == "delete" {
log.Println("Deleting Absence!", "Not implemented")
// TODO
//absence.Delete()
return nil
}
if absence.Update(newAbsence) {
err = absence.Save()
@@ -268,5 +266,4 @@ func updateAbsence(r *http.Request) error {
}
}
return nil
}

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
// 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 (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"arbeitszeitmessung/templates"
"context"
@@ -8,6 +13,23 @@ import (
"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
func changePassword(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()

View File

@@ -1,8 +1,18 @@
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 (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"arbeitszeitmessung/templates"
"context"
"log"
"net/http"
"strconv"
"time"
"github.com/alexedwards/scs/v2"
)
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) {
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)
}
var Session *scs.SessionManager
func CreateSessionManager(lifetime time.Duration) *scs.SessionManager {
Session = scs.New()
Session.Lifetime = lifetime
return Session
}
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)
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,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
import (
@@ -14,8 +17,8 @@ type FileLog struct {
var Logs map[string]FileLog = make(map[string]FileLog)
func NewAudit() (i *log.Logger, close func() error) {
LOG_FILE := "logs/" + time.Now().Format(time.DateOnly) + ".log"
logFile, err := os.OpenFile(LOG_FILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
logName := "logs/" + time.Now().Format(time.DateOnly) + ".log"
logFile, err := os.OpenFile(logName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Panic(err)
}

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

View File

@@ -1,7 +1,10 @@
package helper
// time helpers
import (
"fmt"
"slices"
"time"
)
@@ -16,6 +19,64 @@ func GetMonday(ts time.Time) time.Time {
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 {
if 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
}
func GetKW(t time.Time) int {
_, kw := t.ISOWeek()
return kw
}
func FormatDuration(d time.Duration) string {
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) {
testCases := []struct {
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
import "time"
type TimeFormValue struct {
TsFrom time.Time
TsTo time.Time
CardUID string
}
// type conversion
func BoolToInt(b bool) int {
var i int = 0

View File

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

View File

@@ -1,5 +1,7 @@
package main
// this is the main file where the webserver is configured and all endpoints are added with their routes
import (
"arbeitszeitmessung/endpoints"
"arbeitszeitmessung/helper"
@@ -36,7 +38,7 @@ func main() {
if err != nil {
slog.Info("No .env file found in directory!")
}
if helper.GetEnv("GO_ENV", "production") == "debug" {
if helper.IsDebug() {
logLevel.Set(slog.LevelDebug)
envs := os.Environ()
slog.Debug("Debug mode enabled", "Environment Variables", envs)
@@ -50,11 +52,15 @@ func main() {
defer models.DB.(*sql.DB).Close()
models.Options = configure()
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"))
endpoints.CreateSessionManager(24 * time.Hour)
@@ -63,17 +69,15 @@ func main() {
// handles the different http routes
server.HandleFunc("/time/new", endpoints.TimeCreateHandler)
server.Handle("/absence", ParamsMiddleware(endpoints.AbsencHandler))
server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler))
server.Handle("/absence", paramsMiddleware(endpoints.AbsencHandler))
server.Handle("/time", paramsMiddleware(endpoints.TimeHandler))
server.HandleFunc("/auto/logout", endpoints.LogoutHandler)
server.HandleFunc("/auto/kurzarbeit", endpoints.KurzarbeitFillHandler)
server.HandleFunc("/auto/feiertage", endpoints.FeiertagsHandler)
server.HandleFunc("/user/{action}", endpoints.UserHandler)
// server.HandleFunc("/user/login", endpoints.LoginHandler)
// server.HandleFunc("/user/settings", endpoints.UserSettingsHandler)
server.HandleFunc("/team", endpoints.TeamHandler)
server.HandleFunc("/presence", endpoints.TeamPresenceHandler)
server.Handle("/pdf", ParamsMiddleware(endpoints.PDFFormHandler))
server.HandleFunc("/team/report", endpoints.ReportHandler)
server.HandleFunc("/team/presence", endpoints.PresenceHandler)
server.Handle("/pdf", paramsMiddleware(endpoints.PDFFormHandler))
server.HandleFunc("/pdf/generate", endpoints.PDFCreateController)
server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect))
server.Handle("/static/", http.StripPrefix("/static/", fs))
@@ -87,7 +91,7 @@ func main() {
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) {
queryParams := r.URL.Query()
ctx := context.WithValue(r.Context(), "urlParams", queryParams)
@@ -112,3 +116,10 @@ func loggingMiddleware(next http.Handler) http.Handler {
slog.Info("Completet Request", slog.String("Time", time.Since(start).String()))
})
}
func configure() models.BookingOptions {
return models.BookingOptions{
AllowOutOfBounds: helper.GetEnv("BOOKING_OUT_OF_BOUNDS", "false") == "true",
AllowUnknownUser: helper.GetEnv("BOOKING_FOR_UNKNOWN_USER", "false") == "true",
}
}

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
// 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 (
"database/sql"
"encoding/json"
"errors"
"log"
"log/slog"
"time"
)
@@ -27,25 +40,6 @@ func (a *Absence) IsEmpty() bool {
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 {
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)
case WorktimeBaseWeek:
if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit {
return u.ArbeitszeitProTagFrac(0.2)
return u.ArbeitszeitProWocheFrac(0.2)
} else if a.AbwesenheitTyp.WorkTime <= 0 {
return 0
}
@@ -303,3 +297,24 @@ func (a *Absence) Delete() error {
_, err = qStr.Exec(a.CounterId)
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
// all the files with *_test.go are used to test the functions inside the respective file
// the tests are partially complete
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"

View File

@@ -1,5 +1,11 @@
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 (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/logs"
@@ -30,6 +36,12 @@ type Booking struct {
Timestamp time.Time `json:"timestamp"`
CounterId int `json:"counter_id"`
BookingType BookingType `json:"anwesenheit_typ"`
Valid bool `json:"valid"`
}
type BookingOptions struct {
AllowOutOfBounds bool
AllowUnknownUser bool
}
type IDatabase interface {
@@ -39,7 +51,9 @@ type IDatabase interface {
var DB IDatabase
func (b *Booking) New(cardUid string, gereatId int16, checkInOut int16, typeId int8) Booking {
var Options BookingOptions
func (b *Booking) NewBooking(cardUid string, gereatId int16, checkInOut int16, typeId int8) Booking {
bookingType, err := GetBookingTypeById(typeId)
if err != nil {
log.Printf("Cannot get booking type %d, from database!", typeId)
@@ -85,31 +99,44 @@ func (b *Booking) Verify() bool {
} else {
b.BookingType.Name = bookingType.Name
}
user, err := GetUserByCardUID(b.CardUID)
if err == sql.ErrNoRows {
log.Println("Cannot find user with given CardUID")
return Options.AllowUnknownUser // if allow do not fail verify if not allow fail verify
}
if err != nil {
slog.Error("Cannot get user from CardUID", "error", err)
return false
}
if bookingOutOfBounds(b, &user) {
auditLog, closeLog := logs.NewAudit()
defer closeLog()
if !Options.AllowOutOfBounds {
return false
}
oldTime := b.Timestamp
if oldTime.IsZero() {
oldTime = time.Now()
}
if b.CheckInOut%2 == 1 && b.CheckInOut < 200 { //kommen Booking
b.Timestamp = user.ArbeitMinStartTime(oldTime)
} else {
b.Timestamp = user.ArbeitMaxEndeTime(oldTime)
}
auditLog.Printf("Buchung (%s) von '%s' außerhalb der regulaeren Zeit. Verschieben der Zeit %s -> %s", b.GetBookingType(), user.CardUID, oldTime.Format(time.TimeOnly), b.Timestamp.Format(time.TimeOnly))
slog.Info("Booking is out of work time bounds, setting time to match worktime bounds", "new_time", b.Timestamp.String(), "old_time", oldTime)
}
return true
}
func (b *Booking) 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 {
if !b.Timestamp.IsZero() {
return b.InsertWithTimestamp()
}
if !checkLastBooking(*b) {
return SameBookingError{}
}
@@ -202,7 +229,7 @@ func (b Booking) Save() {
}
func (b *Booking) GetBookingType() string {
debug := (helper.GetEnv("GO_ENV", "production") == "debug")
debug := helper.IsDebug()
switch b.CheckInOut {
case 1: //manuelle Änderung
return "kommen"
@@ -238,20 +265,22 @@ func (b *Booking) Update(nb Booking) {
b.GeraetID = nb.GeraetID
}
if b.Timestamp != nb.Timestamp {
auditLog.Printf("Änderung in Buchung %d von '%s': Buchungszeit (%s -> %s).", b.CounterId, b.CardUID, b.Timestamp.Format("15:04"), nb.Timestamp.Format("15:04)"))
auditLog.Printf("Änderung in Buchung %d von '%s': Buchungszeit (%s -> %s).", b.CounterId, b.CardUID, b.Timestamp.Format(time.TimeOnly), nb.Timestamp.Format(time.TimeOnly))
b.Timestamp = nb.Timestamp
}
}
func checkLastBooking(b Booking) bool {
var check_in_out int
slog.Info("Checking with timestamp:", "timestamp", b.Timestamp.String())
stmt, err := DB.Prepare((`SELECT check_in_out FROM "anwesenheit" WHERE "card_uid" = $1 AND "timestamp"::DATE <= $2::DATE ORDER BY "timestamp" DESC LIMIT 1;`))
var timestamp time.Time
slog.Debug("Checking with timestamp:", "timestamp", b.Timestamp)
stmt, err := DB.Prepare((`SELECT check_in_out, timestamp FROM "anwesenheit" WHERE "card_uid" = $1 AND "timestamp" <= $2 ORDER BY "timestamp" DESC LIMIT 1;`))
if err != nil {
log.Fatalf("Error preparing query: %v", err)
return false
}
err = stmt.QueryRow(b.CardUID, b.Timestamp).Scan(&check_in_out)
err = stmt.QueryRow(b.CardUID, b.Timestamp).Scan(&check_in_out, &timestamp)
slog.Info("Checking last bookings check_in_out", "Check", check_in_out)
if err == sql.ErrNoRows {
return true
}
@@ -259,9 +288,13 @@ func checkLastBooking(b Booking) bool {
log.Println("Error checking last booking: ", err)
return false
}
if int16(check_in_out)%2 == b.CheckInOut%2 {
return false
}
if timestamp.Equal(b.Timestamp) {
return false
}
return true
}
@@ -270,8 +303,6 @@ func (b *Booking) UpdateTime(newTime time.Time) {
if hour == b.Timestamp.Hour() && minute == b.Timestamp.Minute() {
return
}
// TODO: add check for time overlap
var newBooking Booking
newBooking.Timestamp = time.Date(b.Timestamp.Year(), b.Timestamp.Month(), b.Timestamp.Day(), hour, minute, 0, 0, b.Timestamp.Location())
if b.CheckInOut < 3 {
@@ -281,14 +312,11 @@ func (b *Booking) UpdateTime(newTime time.Time) {
newBooking.CheckInOut = 4
}
b.Update(newBooking)
// TODO Check verify
if b.Verify() {
b.Save()
} else {
log.Println("Cannot save updated booking!", b.ToString())
}
// b.Verify()
// b.Save()
}
func (b *Booking) ToString() string {
@@ -340,3 +368,12 @@ func GetBookingTypesCached() []BookingType {
}
return types.([]BookingType)
}
func bookingOutOfBounds(b *Booking, u *User) bool {
bookingTime := b.Timestamp
if b.Timestamp.IsZero() {
bookingTime = time.Now()
}
res := bookingTime.Before(u.ArbeitMinStartTime(bookingTime)) || bookingTime.After(u.ArbeitMaxEndeTime(bookingTime))
return res
}

View File

@@ -13,35 +13,250 @@ var testBookingType = models.BookingType{
var testBookings8hrs = []models.Booking{{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")),
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
}, {
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 16:00")),
Timestamp: time.Date(2025, 01, 01, 16, 0, 0, 0, time.UTC),
BookingType: testBookingType,
}}
var testBookings6hrs = []models.Booking{{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")),
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
}, {
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 14:00")),
Timestamp: time.Date(2025, 01, 01, 14, 0, 0, 0, time.UTC),
BookingType: testBookingType,
}}
var testBookings10hrs = []models.Booking{{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")),
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
}, {
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 18:00")),
Timestamp: time.Date(2025, 01, 01, 18, 0, 0, 0, time.UTC),
BookingType: testBookingType,
}}
var testBookings6hrsBreak30min = []models.Booking{
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 9, 30, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 14, 30, 0, 0, time.UTC),
BookingType: testBookingType,
}}
var testBookings610hrsBreak30min = []models.Booking{
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 9, 30, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 14, 40, 0, 0, time.UTC),
BookingType: testBookingType,
}}
var testBookings9hrsBreak30min = []models.Booking{
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 9, 30, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 17, 30, 0, 0, time.UTC),
BookingType: testBookingType,
}}
var testBookings930hrs = []models.Booking{
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 17, 30, 0, 0, time.UTC),
BookingType: testBookingType,
}}
var testBookings910hrsBreak30min = []models.Booking{
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 9, 30, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 17, 40, 0, 0, time.UTC),
BookingType: testBookingType,
},
}
var testBookings910hrsBreak35min = []models.Booking{
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 9, 35, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 17, 45, 0, 0, time.UTC),
BookingType: testBookingType,
},
}
var testBookings945hrs = []models.Booking{
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 17, 45, 0, 0, time.UTC),
BookingType: testBookingType,
},
}
var testBookings10hrsBreak45min = []models.Booking{
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 9, 45, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 18, 00, 0, 0, time.UTC),
BookingType: testBookingType,
},
}
var testBookings1030hrsBreak45min = []models.Booking{
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 1,
Timestamp: time.Date(2025, 01, 01, 9, 45, 0, 0, time.UTC),
BookingType: testBookingType,
},
{
CardUID: "aaaa-aaaa",
CheckInOut: 2,
Timestamp: time.Date(2025, 01, 01, 18, 30, 0, 0, time.UTC),
BookingType: testBookingType,
},
}

View File

@@ -1,5 +1,12 @@
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 (
"log/slog"
"time"
@@ -10,6 +17,15 @@ type CompoundDay struct {
DayParts []IWorkDay
}
// IsSubmittedAndAccepted implements IWorkDay.
func (c *CompoundDay) IsSubmittedAndAccepted() bool {
var isSubmittedAndAccepted = true
for _, day := range c.DayParts {
isSubmittedAndAccepted = isSubmittedAndAccepted && day.IsSubmittedAndAccepted()
}
return isSubmittedAndAccepted
}
func NewCompondDay(date time.Time, dayParts ...IWorkDay) *CompoundDay {
return &CompoundDay{Day: date, DayParts: dayParts}
}

View File

@@ -1,5 +1,8 @@
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 (
"arbeitszeitmessung/helper"
)

View File

@@ -1,5 +1,10 @@
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 (
"log/slog"
"time"
@@ -18,6 +23,7 @@ type IWorkDay interface {
GetTimes(User, WorktimeBase, bool) (work, pause, overtime time.Duration)
GetOvertime(User, WorktimeBase, bool) time.Duration
IsEmpty() bool
IsSubmittedAndAccepted() bool
}
type DayType int
@@ -49,7 +55,9 @@ func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay
}
for _, absentDay := range absences {
if weekDay := absentDay.Date().Weekday(); weekDay == time.Saturday || weekDay == time.Sunday {
continue
}
// Check if there is already a day
existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)]
switch {

View File

@@ -1,5 +1,10 @@
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 (
"time"
@@ -14,6 +19,11 @@ type PublicHoliday struct {
worktime int8
}
// IsSubmittedAndAccepted implements IWorkDay.
func (p *PublicHoliday) IsSubmittedAndAccepted() bool {
return true
}
// IsEmpty implements [IWorkDay].
func (p *PublicHoliday) IsEmpty() bool {
return false

View File

@@ -1,5 +1,11 @@
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 (
"arbeitszeitmessung/helper"
"context"
@@ -22,12 +28,14 @@ type User struct {
ArbeitszeitPerTag float32 //`json:"arbeitszeit_per_tag"`
ArbeitszeitPerWoche float32 //`json:"arbeitszeit_per_woche"`
Overtime time.Duration
ArbeitMinStart time.Time
ArbeitMaxEnde time.Time
}
func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) {
var user User
var err error
if helper.GetEnv("GO_ENV", "production") == "debug" {
if helper.IsDebug() {
user, err = GetUserByPersonalNr(123)
} else {
if !Session.Exists(ctx, "user") {
@@ -44,50 +52,56 @@ func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User,
}
// 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
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 {
return 0, err
}
defer qStr.Close()
err = qStr.QueryRow(u.PersonalNummer).Scan(&overtime)
err = qStr.QueryRow(u.PersonalNummer, startDate).Scan(&overtime)
if err != nil {
return 0, err
}
return overtime, nil
}
func GetAllUsers() ([]User, error) {
qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname,arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten;`))
var users []User
if err != nil {
return users, err
}
defer qStr.Close()
rows, err := qStr.Query()
if err != nil {
return users, err
}
defer rows.Close()
for rows.Next() {
func GetUserByCardUID(cardUid string) (User, error) {
var user User
if err := rows.Scan(&user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche); err != nil {
log.Println("Error creating user!", err)
continue
qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten WHERE card_uid = $1;`))
if err != nil {
return user, err
}
users = append(users, user)
err = qStr.QueryRow(cardUid).Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde)
if err != nil {
return user, err
}
if err = rows.Err(); err != nil {
return users, nil
}
return users, nil
return user, nil
}
func (u *User) GetAll() ([]User, error) {
qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname FROM s_personal_daten;`))
func (u *User) ArbeitMinStartTime(date time.Time) time.Time {
if date.Hour() > 0 {
date = date.Truncate(24 * time.Hour).Add(-time.Hour)
}
date = date.Truncate(time.Hour)
slog.Info("Date truncate", "date", date)
return date.Add(time.Hour*time.Duration(u.ArbeitMinStart.Hour()) + time.Minute*time.Duration(u.ArbeitMinStart.Minute()))
}
func (u *User) ArbeitMaxEndeTime(date time.Time) time.Time {
if date.Hour() > 0 {
date = date.Truncate(24 * time.Hour).Add(-time.Hour)
}
date = date.Truncate(time.Hour)
slog.Info("Date truncate", "date", date)
return date.Add(time.Hour*time.Duration(u.ArbeitMaxEnde.Hour()) + time.Minute*time.Duration(u.ArbeitMaxEnde.Minute()))
}
func GetAllUsers() ([]User, error) {
qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten;`))
var users []User
if err != nil {
return users, err
@@ -101,7 +115,7 @@ func (u *User) GetAll() ([]User, error) {
for rows.Next() {
var user User
if err := rows.Scan(&user.CardUID, &user.Vorname, &user.Name); err != nil {
if err := rows.Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde); err != nil {
log.Println("Error creating user!", err)
continue
}
@@ -149,7 +163,7 @@ func (u *User) CheckAnwesenheit() bool {
// Creates a new booking for the user -> check_in_out will be 254 for automatic check out
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()
if err != nil {
fmt.Printf("Error inserting booking %v -> %v\n", booking, err)
@@ -161,11 +175,11 @@ func (u *User) CheckOut() error {
func GetUserByPersonalNr(personalNummer int) (User, error) {
var user User
qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten WHERE personal_nummer = $1;`))
qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten WHERE personal_nummer = $1;`))
if err != nil {
return user, err
}
err = qStr.QueryRow(personalNummer).Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche)
err = qStr.QueryRow(personalNummer).Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde)
if err != nil {
return user, err
@@ -179,7 +193,7 @@ func GetUserByPersonalNrMulti(personalNummerMulti []int) ([]User, error) {
return users, errors.New("No personalNumbers provided")
}
qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten WHERE personal_nummer = ANY($1::int[]);`))
qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten WHERE personal_nummer = ANY($1::int[]);`))
if err != nil {
return users, err
}
@@ -194,7 +208,7 @@ func GetUserByPersonalNrMulti(personalNummerMulti []int) ([]User, error) {
defer rows.Close()
for rows.Next() {
var user User
if err := rows.Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche); err != nil {
if err := rows.Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde); err != nil {
return users, err
}
users = append(users, user)
@@ -240,6 +254,7 @@ func (u *User) ChangePass(password, newPassword string) (bool, error) {
}
func (u *User) GetTeamMembers() ([]User, error) {
var teamMemberPNrs []int
var teamMembers []User
qStr, err := DB.Prepare(`SELECT personal_nummer FROM s_personal_daten WHERE vorgesetzter_pers_nr = $1 ORDER BY "nachname";`)
if err != nil {
@@ -255,12 +270,16 @@ func (u *User) GetTeamMembers() ([]User, error) {
for rows.Next() {
var personalNr int
err := rows.Scan(&personalNr)
user, err := GetUserByPersonalNr(personalNr)
teamMemberPNrs = append(teamMemberPNrs, personalNr)
if err != nil {
log.Println("Error getting user!")
return teamMembers, err
}
teamMembers = append(teamMembers, user)
}
teamMembers, err = GetUserByPersonalNrMulti(teamMemberPNrs)
if err != nil {
log.Println("Error getting users!")
return teamMembers, err
}
return teamMembers, nil
@@ -282,23 +301,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
func (u *User) GetLastWorkWeekSubmission() time.Time {
var lastSub time.Time
qStr, err := DB.Prepare(`
SELECT COALESCE(
(SELECT woche_start + INTERVAL '1 week' FROM wochen_report WHERE personal_nummer = $1 ORDER BY woche_start DESC LIMIT 1),
(SELECT timestamp FROM anwesenheit WHERE card_uid = $2 ORDER BY timestamp LIMIT 1)
) AS letzte_buchung;
SELECT new_week
FROM (
-- Highest priority
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 {
slog.Debug("Error preparing query statement.", "error", err)
@@ -314,22 +356,6 @@ func (u *User) GetLastWorkWeekSubmission() time.Time {
return lastSub
}
func (u *User) GetFromCardUID(card_uid string) (User, error) {
user := User{}
var err error
qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag FROM s_personal_daten WHERE card_uid = $1;`))
if err != nil {
return user, err
}
err = qStr.QueryRow(card_uid).Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag)
if err != nil {
return user, err
}
return user, nil
}
func (u *User) IsSuperior(e User) bool {
var isSuperior int
qStr, err := DB.Prepare(`SELECT COUNT(1) FROM s_personal_daten WHERE personal_nummer = $1 AND vorgesetzter_pers_nr = $2`)
@@ -343,7 +369,6 @@ func (u *User) IsSuperior(e User) bool {
return false
}
return isSuperior == 1
}
func getMonday(ts time.Time) time.Time {

View File

@@ -1,13 +1,21 @@
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 (
"arbeitszeitmessung/helper"
"database/sql"
"encoding/json"
"fmt"
"log"
"log/slog"
"sort"
"time"
"github.com/lib/pq"
)
type WorkDay struct {
@@ -41,11 +49,10 @@ func (d *WorkDay) GetWorktimeAbsence() Absence {
// Gets the time as is in the db (with corrected pause times)
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)
}
work, pause := calcWorkPause(d.Bookings)
work, pause = correctWorkPause(work, pause)
work, _ := correctWorkPause(getWorkPause(d))
if (d.worktimeAbsece != Absence{}) {
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
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)
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)
}
func getWorkPause(d *WorkDay) (work, pause time.Duration) {
//if today calc, else take from db
if d.workTime == 0 && d.pauseTime == 0 && len(d.Bookings) > 0 {
return calcWorkPause(d.Bookings)
} else {
return d.workTime, d.pauseTime
}
}
func calcWorkPause(bookings []Booking) (work, pause time.Duration) {
var lastBooking Booking
for _, b := range bookings {
@@ -100,7 +116,8 @@ func correctWorkPause(workIn, pauseIn time.Duration) (work, pause time.Duration)
}
var diff time.Duration
if workIn <= (9*time.Hour) && pauseIn < 30*time.Minute {
if (workIn+pauseIn) <= (9*time.Hour+30*time.Minute) && pauseIn <= 30*time.Minute {
diff = 30*time.Minute - pauseIn
} else if pauseIn < 45*time.Minute {
diff = 45*time.Minute - pauseIn
@@ -135,12 +152,21 @@ func (d *WorkDay) Type() DayType {
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
if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() {
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)
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())
@@ -153,7 +179,7 @@ func (d *WorkDay) GetKurzArbeit() *Absence {
}
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 {
@@ -173,11 +199,16 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
var workSec, pauseSec float64
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 (
WITH
all_days AS (
SELECT
generate_series(
$2 ::DATE,
$3 ::DATE - INTERVAL '1 day',
INTERVAL '1 day'
)::DATE AS work_date
),
all_bookings AS (
SELECT
a.card_uid,
a.timestamp,
@@ -187,82 +218,128 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
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
PARTITION BY
a.card_uid,
a.timestamp::DATE
ORDER BY
a.timestamp
) AS prev_check,
LAG(a.timestamp) OVER (
PARTITION BY
a.card_uid,
a.timestamp::DATE
ORDER BY
a.timestamp
) AS prev_timestamp
FROM normalized_bookings
)
FROM
anwesenheit a
LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id
WHERE
a.card_uid = $1
AND a.timestamp::DATE >= $2::DATE
AND a.timestamp::DATE <= $3::DATE
),
normalized_bookings AS (
SELECT
*
FROM
all_bookings
WHERE
prev_check IS NULL
OR prev_check <> check_in_out
)
SELECT
d.work_date,
COALESCE(MIN(b.timestamp), NOW()) AS time_from,
COALESCE(MAX(b.timestamp), NOW()) AS time_to,
COALESCE(
EXTRACT(EPOCH FROM SUM(
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
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;`)
) 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),
// ordered_bookings AS (
// normalized_bookings AS (
// SELECT *
// FROM (
// SELECT
// a.timestamp::DATE AS work_date,
// 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.timestamp) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_timestamp,
// LAG(a.check_in_out) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_check
// 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
// 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,
@@ -317,26 +394,29 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
var workDay WorkDay
var bookings []byte
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
}
workDay.workTime = time.Duration(workSec * float64(time.Second))
workDay.pauseTime = time.Duration(pauseSec * float64(time.Second))
if bookings != nil {
err = json.Unmarshal(bookings, &workDay.Bookings)
if err != nil {
log.Println("Error parsing bookings JSON!", err)
slog.Error("Error parsing bookings JSON!", "Error", err, "Json", bookings)
return nil
}
// better empty day handling
if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 {
workDay.Bookings = []Booking{}
}
// better empty day handling
// if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 {
// workDay.Bookings = []Booking{}
// }
if len(workDay.Bookings) >= 1 || !helper.IsWeekend(workDay.Date()) {
workDays = append(workDays, workDay)
}
}
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
@@ -344,10 +424,12 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
// returns bool wheter the workday was ended with an automatic logout
func (d *WorkDay) RequiresAction() bool {
if len(d.Bookings) == 0 {
return false
for i := range d.Bookings {
if d.Bookings[i].CheckInOut > 250 {
return true
}
return d.Bookings[len(d.Bookings)-1].CheckInOut == 254
}
return false
}
func (d *WorkDay) GetDayProgress(u User) int8 {
@@ -358,3 +440,38 @@ func (d *WorkDay) GetDayProgress(u User) int8 {
progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100
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

@@ -16,10 +16,10 @@ func CatchError[T any](val T, err error) T {
}
var testWorkDay = models.WorkDay{
Day: CatchError(time.Parse(time.DateOnly, "2025-01-01")),
Day: time.Date(2025, 01, 01, 0, 0, 0, 0, time.Local),
Bookings: testBookings8hrs,
TimeFrom: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")),
TimeTo: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 16:30")),
TimeFrom: time.Date(2025, 01, 01, 8, 0, 0, 0, time.Local),
TimeTo: time.Date(2025, 01, 01, 16, 30, 0, 0, time.Local),
}
func TestWorkdayWorktimeDay(t *testing.T) {
@@ -30,18 +30,63 @@ func TestWorkdayWorktimeDay(t *testing.T) {
}{
{
testName: "Bookings6hrs",
bookings: testBookings6hrs,
expectedTime: time.Hour * 6,
bookings: testBookings6hrs, //work 6h
expectedTime: time.Hour * 6, //pause 0
},
{
testName: "Bookings8hrs",
bookings: testBookings8hrs,
expectedTime: time.Hour*7 + time.Minute*30,
bookings: testBookings8hrs, //work 8 pause 0
expectedTime: time.Hour*7 + time.Minute*30, //pause 30 --> corrected
},
{
testName: "Bookings10hrs",
bookings: testBookings10hrs,
expectedTime: time.Hour*9 + time.Minute*15,
bookings: testBookings10hrs, //work 10 pause 0
expectedTime: time.Hour*9 + time.Minute*15, //pause 45 --> corrected
},
{
testName: "Booking 6h with 30 min Break",
bookings: testBookings6hrsBreak30min, //work 6 pause 30
expectedTime: time.Hour * 6, //pause 30 --> bc real pause
},
{
testName: "Booking 6h 10min with 30 min Break",
bookings: testBookings610hrsBreak30min, //work 6 10 pause 30
expectedTime: time.Hour*6 + time.Minute*10, //pause 30 --> real pause
},
{
testName: "Booking 9h with 30 min Break",
bookings: testBookings9hrsBreak30min, //work 9 pause 30
expectedTime: time.Hour * 9, //pause 30 --> real pause
},
{
testName: "Booking 9h 30min",
bookings: testBookings930hrs, //work 9 30 pause 0
expectedTime: time.Hour * 9, //pause 30 --> corrected
},
{
testName: "Booking 9h 40min with 30min Break",
bookings: testBookings910hrsBreak30min, //work 9 10 pause 30
expectedTime: time.Hour*8 + time.Minute*55, //pause 45 --> real + corrected
},
{
testName: "Booking 9h 40min with 35min Break",
bookings: testBookings910hrsBreak35min, //work 9 10 pause 35
expectedTime: time.Hour * 9, //pause 45 --> real + corrected
},
{
testName: "Booking 9h 45min",
bookings: testBookings945hrs, //work 9 45 pause 0
expectedTime: time.Hour * 9, //pause 45 --> corrected
},
{
testName: "Booking 10h Break 45min",
bookings: testBookings10hrsBreak45min, //work 9 15 pause 45
expectedTime: time.Hour*9 + time.Minute*15, //pause 45 --> real
},
{
testName: "Booking 10h 30min Break 45min",
bookings: testBookings1030hrsBreak45min, //work 9 45 pause 45
expectedTime: time.Hour*9 + time.Minute*45, //pause 45 --> real
},
}
@@ -113,6 +158,51 @@ func TestWorkdayPausetimeDay(t *testing.T) {
bookings: testBookings10hrs,
expectedTime: time.Minute * 45,
},
{
testName: "Booking 6h with 30 min Break",
bookings: testBookings6hrsBreak30min, //work 6 pause 30
expectedTime: time.Minute * 30, //pause 30 --> bc real pause
},
{
testName: "Booking 6h 10min with 30 min Break",
bookings: testBookings610hrsBreak30min, //work 6 10 pause 30
expectedTime: time.Minute * 30, //pause 30 --> real pause
},
{
testName: "Booking 9h with 30 min Break",
bookings: testBookings9hrsBreak30min, //work 9 pause 30
expectedTime: time.Minute * 30, //pause 30 --> real pause
},
{
testName: "Booking 9h 30min",
bookings: testBookings930hrs, //work 9 30 pause 0
expectedTime: time.Minute * 30, //pause 30 --> corrected
},
{
testName: "Booking 9h 40min with 30min Break",
bookings: testBookings910hrsBreak30min, //work 9 10 pause 30
expectedTime: time.Minute * 45, //pause 45 --> real + corrected
},
{
testName: "Booking 9h 40min with 35min Break",
bookings: testBookings910hrsBreak35min, //work 9 10 pause 35
expectedTime: time.Minute * 45, //pause 45 --> real + corrected
},
{
testName: "Booking 9h 45min",
bookings: testBookings945hrs, //work 9 45 pause 0
expectedTime: time.Minute * 45, //pause 45 --> corrected
},
{
testName: "Booking 10h Break 45min",
bookings: testBookings10hrsBreak45min, //work 9 15 pause 45
expectedTime: time.Minute * 45, //pause 45 --> real
},
{
testName: "Booking 10h 30min Break 45min",
bookings: testBookings1030hrsBreak45min, //work 9 45 pause 45
expectedTime: time.Minute * 45, //pause 45 --> real
},
}
for _, tc := range testCases {

View File

@@ -1,6 +1,12 @@
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 (
"arbeitszeitmessung/helper"
"database/sql"
"errors"
"log"
@@ -19,25 +25,34 @@ type WorkWeek struct {
Days []IWorkDay
User User
WeekStart time.Time
weekEnd time.Time
Worktime time.Duration
WorktimeVirtual time.Duration
Overtime time.Duration
Status WeekStatus
WeekBase WorktimeBase
Kurzarbeit time.Duration
}
type WeekStatus int8
const (
WeekStatusNone WeekStatus = iota
WeekStatusCorrected
WeekStatusSent
WeekStatusAccepted
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{
User: user,
WeekStart: tsMonday,
WeekStart: tsStart,
weekEnd: tsEnd,
Status: WeekStatusNone,
}
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) {
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()))
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 {
w.Worktime += day.GetWorktime(w.User, WorktimeBaseDay, false)
w.WorktimeVirtual += day.GetWorktime(w.User, WorktimeBaseDay, true)
dWorkTime := day.GetWorktime(w.User, w.WeekBase, false)
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())
@@ -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 {
if w.Status != WeekStatusNone {
return w.Status
@@ -81,25 +118,31 @@ func (w *WorkWeek) CheckStatus() WeekStatus {
log.Println("Cannot access Database!")
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 {
log.Println("Error preparing SQL statement", err)
return w.Status
}
defer qStr.Close()
var beastatigt bool
err = qStr.QueryRow(w.WeekStart, w.User.PersonalNummer).Scan(&beastatigt)
var beastatigt sql.NullBool
err = qStr.QueryRow(w.WeekStart, w.User.PersonalNummer).Scan(&beastatigt, &w.Id)
if err == sql.ErrNoRows {
return w.Status
}
slog.Info("Bestätigt query res", "Best", beastatigt, "week", w.Id)
if err != nil {
log.Println("Error querying database", err)
return w.Status
}
if beastatigt {
switch {
case beastatigt.Bool:
w.Status = WeekStatusAccepted
} else {
case beastatigt.Valid:
w.Status = WeekStatusSent
default:
w.Status = WeekStatusCorrected
}
return w.Status
}
@@ -201,23 +244,33 @@ func (w *WorkWeek) SendWeek() error {
return ErrRunningWeek
}
if w.CheckStatus() != WeekStatusNone {
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
}
} else {
switch w.CheckStatus() {
case WeekStatusNone:
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 {
slog.Warn("Error preparing SQL statement", "error", 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))
if err != nil {
log.Println("Error executing query!", err)
slog.Error("Error executing query!", "error", err)
return err
}
return nil

View File

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

View File

@@ -78,14 +78,8 @@
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-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));
}

View File

@@ -20,7 +20,6 @@
--color-neutral-300: oklch(87% 0 0);
--color-neutral-400: oklch(70.8% 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-800: oklch(26.9% 0 0);
--color-black: #000;
@@ -30,8 +29,6 @@
--text-sm--line-height: calc(1.25 / 0.875);
--text-xl: 1.25rem;
--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;
--radius-md: 0.375rem;
--default-transition-duration: 150ms;
@@ -205,45 +202,24 @@
.top-0 {
top: calc(var(--spacing) * 0);
}
.top-1 {
top: calc(var(--spacing) * 1);
}
.top-1\/2 {
top: calc(1/2 * 100%);
}
.top-2 {
top: calc(var(--spacing) * 2);
}
.top-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;
}
.right-1 {
right: calc(var(--spacing) * 1);
}
.right-2 {
right: calc(var(--spacing) * 2);
}
.right-2\.5 {
right: calc(var(--spacing) * 2.5);
}
.left-1 {
left: calc(var(--spacing) * 1);
}
.left-1\/2 {
left: calc(1/2 * 100%);
}
.z-10 {
z-index: 10;
}
.z-100 {
z-index: 100;
}
@@ -262,6 +238,9 @@
.-my-1 {
margin-block: calc(var(--spacing) * -1);
}
.my-2 {
margin-block: calc(var(--spacing) * 2);
}
.mt-1 {
margin-top: calc(var(--spacing) * 1);
}
@@ -329,6 +308,32 @@
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");
}
.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\] {
display: inline-block;
width: 1.25em;
@@ -371,6 +376,9 @@
.block {
display: block;
}
.contents {
display: contents;
}
.flex {
display: flex;
}
@@ -401,12 +409,13 @@
width: calc(var(--spacing) * 5);
height: calc(var(--spacing) * 5);
}
.size-6 {
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
}
.h-2 {
height: calc(var(--spacing) * 2);
}
.h-3 {
height: calc(var(--spacing) * 3);
}
.h-3\.5 {
height: calc(var(--spacing) * 3.5);
}
@@ -431,9 +440,6 @@
.w-2 {
width: calc(var(--spacing) * 2);
}
.w-3 {
width: calc(var(--spacing) * 3);
}
.w-3\.5 {
width: calc(var(--spacing) * 3.5);
}
@@ -443,9 +449,6 @@
.w-5 {
width: calc(var(--spacing) * 5);
}
.w-9 {
width: calc(var(--spacing) * 9);
}
.w-9\/10 {
width: calc(9/10 * 100%);
}
@@ -458,9 +461,6 @@
.w-full {
width: 100%;
}
.flex-shrink {
flex-shrink: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
@@ -476,21 +476,10 @@
.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 {
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
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 {
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -501,36 +490,18 @@
.cursor-pointer {
cursor: pointer;
}
.resize {
resize: both;
}
.scroll-m-2 {
scroll-margin: calc(var(--spacing) * 2);
}
.appearance-none {
appearance: none;
}
.break-after-page {
break-after: page;
}
.auto-rows-min {
grid-auto-rows: min-content;
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-5 {
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-direction: column;
}
@@ -581,11 +552,6 @@
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: flex-end;
}
@@ -612,18 +578,10 @@
border-style: var(--tw-border-style);
border-width: 0px;
}
.border-r-0 {
border-right-style: var(--tw-border-style);
border-right-width: 0px;
}
.border-r-1 {
border-right-style: var(--tw-border-style);
border-right-width: 1px;
}
.border-b-0 {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 0px;
}
.border-dashed {
--tw-border-style: dashed;
border-style: dashed;
@@ -634,9 +592,6 @@
.border-neutral-500 {
border-color: var(--color-neutral-500);
}
.border-neutral-600 {
border-color: var(--color-neutral-600);
}
.border-slate-800 {
border-color: var(--color-slate-800);
}
@@ -661,18 +616,12 @@
.bg-red-600 {
background-color: var(--color-red-600);
}
.mask-repeat {
mask-repeat: repeat;
}
.p-1 {
padding: calc(var(--spacing) * 1);
}
.p-2 {
padding: calc(var(--spacing) * 2);
}
.p-8 {
padding: calc(var(--spacing) * 8);
}
.px-3 {
padding-inline: calc(var(--spacing) * 3);
}
@@ -685,10 +634,6 @@
.text-center {
text-align: center;
}
.text-2xl {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
.text-sm {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
@@ -710,9 +655,6 @@
.text-black {
color: var(--color-black);
}
.text-neutral-300 {
color: var(--color-neutral-300);
}
.text-neutral-500 {
color: var(--color-neutral-500);
}
@@ -740,16 +682,9 @@
.uppercase {
text-transform: uppercase;
}
.underline {
text-decoration-line: underline;
}
.opacity-0 {
opacity: 0%;
}
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.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,);
}
@@ -776,18 +711,6 @@
-webkit-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 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
@@ -1081,7 +1004,7 @@
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-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
input.btn, select.btn {
@@ -1195,11 +1118,6 @@
syntax: "*";
inherits: false;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur {
syntax: "*";
inherits: false;
@@ -1272,7 +1190,6 @@
--tw-border-style: solid;
--tw-divide-y-reverse: 0;
--tw-font-weight: initial;
--tw-outline-style: solid;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;

View File

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

View File

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

View File

@@ -1,9 +1,12 @@
package templates
// 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/helper"
templ Base() {
templ BasePage() {
<!DOCTYPE html>
<head>
<title>Arbeitszeit</title>
@@ -14,7 +17,7 @@ templ Base() {
}
templ LoginPage(success bool, errorMsg string) {
@Base()
@BasePage()
<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>
@@ -31,7 +34,7 @@ templ LoginPage(success bool, errorMsg string) {
templ SettingsPage(status int) {
{{ user := ctx.Value("user").(models.User) }}
@Base()
@BasePage()
@headerComponent()
<div class="grid-main divide-y-1">
<form method="POST" class="grid-sub responsive lg:divide-x-1">
@@ -58,6 +61,8 @@ templ SettingsPage(status int) {
<div class="grid-cell col-span-3">
<p>Nutzername: <span class="text-neutral-500">{ user.Vorname } { user.Name }</span></p>
<p>Personalnummer: <span class="text-neutral-500">{ user.PersonalNummer }</span></p>
<p>Frühester Arbeitsbegin: <span class="text-neutral-500">{ user.ArbeitMinStart.Format("15:06") } Uhr</span></p>
<p>Spätester Arbeitsende: <span class="text-neutral-500">{ user.ArbeitMaxEnde.Format("15:06") } Uhr</span></p>
<p>Arbeitszeit pro Tag: <span class="text-neutral-500">{ helper.FormatDuration(user.ArbeitszeitProTag()) }</span></p>
<p>Arbeitszeit pro Woche: <span class="text-neutral-500">{ helper.FormatDuration(user.ArbeitszeitProWoche()) }</span></p>
</div>
@@ -74,15 +79,3 @@ templ SettingsPage(status int) {
</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
// this file has all the templates for the /team/presence page
import "arbeitszeitmessung/models"
import "arbeitszeitmessung/helper"
templ TeamPresencePage(teamPresence map[models.User]bool) {
@Base()
@BasePage()
@headerComponent()
<div class="grid-main divide-y-1">
<div class="grid-sub divide-x-1 bg-neutral-300">
@@ -27,3 +29,14 @@ templ TeamPresencePage(teamPresence map[models.User]bool) {
}
</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
// this file has all the templates for the team/report page
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
@@ -8,11 +10,11 @@ import (
"time"
)
templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) {
@Base()
templ ReportPage(weeks []models.WorkWeek, userWeek models.WorkWeek) {
@BasePage()
@headerComponent()
<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-main divide-y-1 @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">
<h2 class="text-xl uppercase font-bold">Eigene Abrechnung</h2>
</div>
@@ -34,7 +36,7 @@ templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
year, kw := week.WeekStart.ISOWeek()
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">
if !onlyAccept {
<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">
if !onlyAccept {
<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">
@statusCheckMark(week.CheckStatus(), models.WeekStatusSent)
Gesendet
@@ -58,7 +66,7 @@ templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
<div class="flex flex-row gap-2 col-span-3">
@timeGaugeComponent(int8(progress), false)
<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>
</div>
</div>
@@ -144,3 +152,41 @@ templ weekDayTypeSwitcher(day models.IWorkDay) {
<div>{ day.ToString() }</div>
}
}
templ weekPicker(weekStart time.Time) {
{{ year, kw := weekStart.ISOWeek() }}
<form method="get" class="flex flex-row gap-4 items-center justify-around">
<input type="date" class="hidden" name="submission_date" value={ weekStart.Format(time.DateOnly) }/>
<button onclick={ templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "-1") } class="btn">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="chevron-left size-4 mx-auto" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0"></path>
</svg>
</button>
<p class="whitespace-nowrap">KW { fmt.Sprintf("%02d, %d", kw, year) }</p>
<button disabled?={ time.Since(weekStart) < 24*7*time.Hour } onclick={ templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "1") } class="btn disabled:pointer-events-none disabled:opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="chevron-right size-4 mx-auto" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708"></path>
</svg>
</button>
</form>
}
templ workDayWeekComponent(workDay *models.WorkDay) {
if !workDay.RequiresAction() {
<div class="flex flex-row gap-2 items-center">
<span class="icon-[material-symbols-light--schedule-outline] flex-shrink-0"></span>
switch {
case !workDay.IsEmpty():
<span>{ workDay.TimeFrom.Format("15:04") }</span>
<span>-</span>
<span>{ workDay.TimeTo.Format("15:04") }</span>
case workDay.IsKurzArbeit():
<span>Kurzarbeit</span>
default:
<p>Keine Anwesenheit</p>
}
</div>
} else {
<p class="text-red-600">Bitte anpassen</p>
}
}

View File

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

View File

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

View File

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

View File

@@ -2,24 +2,16 @@ package templates
import (
"arbeitszeitmessung/models"
"fmt"
"strconv"
"time"
)
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 changeButtonComponent(id string, workDay bool) {
templ changeButtonComponent(id string, workDay bool, disabled bool) {
if disabled {
<button class="h-10 change-button-component btn w-auto group/button" type="button" disabled>
<p class="hidden md:block group-[.edit]/button:hidden">Ändern</p>
</button>
} else {
<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>
@@ -29,35 +21,6 @@ templ changeButtonComponent(id string, workDay bool) {
</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 {
<div class={ "w-2 h-full bg-accent rounded-md flex-shrink-0", bgColor }></div>
}
}
@@ -120,23 +83,31 @@ templ newBookingComponent(d models.IWorkDay) {
templ bookingComponent(booking models.Booking) {
<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>
<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() }
</p>
if booking.IsSubmittedAndChecked() {
<p>submitted</p>
if !booking.Valid {
fehlerhafte Buchung, wird nicht zur Berechnung verwendet!
}
</p>
</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 workdayComponent(workDay *models.WorkDay) {
if workDay.IsEmpty() && !workDay.IsKurzArbeit() {
<p class="text group-[.edit]:hidden">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p>
} else {
if workDay.IsKurzArbeit() {
@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,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
// 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 (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
@@ -10,7 +14,7 @@ import (
templ TimePage(workDays []models.WorkDay, lastSub time.Time) {
{{ allDays := ctx.Value("days").([]models.IWorkDay) }}
@Base()
@BasePage()
@headerComponent()
<div class="grid-main divide-y-1">
@inputForm()
@@ -21,7 +25,7 @@ templ TimePage(workDays []models.WorkDay, lastSub time.Time) {
}
}
</div>
@LegendComponent()
@legendComponent()
}
templ inputForm() {
@@ -138,8 +142,11 @@ templ defaultDayComponent(day models.IWorkDay) {
<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">
@changeButtonComponent("time-"+day.Date().Format(time.DateOnly), true)
<div class="grid-cell flex flex-row gap-2 items-end ">
@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>
}
@@ -160,21 +167,3 @@ templ timeDayTypeSwitch(day models.IWorkDay, fromCompound bool) {
<p>{ day.ToString() }</p>
}
}
templ workdayComponent(workDay *models.WorkDay) {
if len(workDay.Bookings) < 1 {
<p class="text group-[.edit]:hidden">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p>
} else {
if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 {
@absenceComponent(workDay.GetKurzArbeit(), true)
}
for _, booking := range workDay.Bookings {
@bookingComponent(booking)
}
<input type="hidden" name="select_kommen" value={ len(workDay.Bookings) > 0 && workDay.Bookings[len(workDay.Bookings)-1].CheckInOut%2 == 0 }/>
}
}
templ holidayComponent(d models.IWorkDay) {
<p>{ d.ToString() }</p>
}

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ echo "Creating PostgreSQL user and setting permissions... $POSTGRES_USER for API
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
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

View File

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

View File

@@ -1,13 +1,16 @@
POSTGRES_USER=root # Postgres ADMIN Nutzername
POSTGRES_USER=root # Postgres ADMIN Nutzername. regex:^\w+$
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). regex:^\w+$
POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung)
POSTGRES_PATH=__ROOT__/DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...)
POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name
POSTGRES_PORT=127.0.0.1:5432 # Postgres Port normalerweise nicht freigegeben. regex:^[0-9]{1,5}$
MIGRATIONS_PATH=__ROOT__/migrations # Pfad zu DB migrations (wenn nicht verändert wurde, bei default bleiben)
POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name. regex:^[a-z]+$
POSTGRES_PORT=5432 # Postgres Port normalerweise nicht freigegeben. regex:^[0-9]{1,5}$
TZ=Europe/Berlin # Zeitzone
API_TOKEN=dont_access # API Token für ESP Endpoints
API_TOKEN=dont_access # API Token für ESP32 Endpoints
WEB_PORT=8000 # Port unter welchem Webserver erreichbar ist. regex:^[0-9]{1,5}$
LOG_PATH=__ROOT__/logs # Pfad für Audit Logs
LOG_LEVEL=warn # Welche Log-Nachrichten werden in der Konsole erscheinen
LOG_LEVEL=warn # Welche Log-Nachrichten werden in der Konsole erscheinen. regex:^(debug|info|warn|error)$
BACKUP_FOLDER=__ROOT__/backup # Pfad für DB Backup Datein
BOOKING_OUT_OF_BOUNDS=true # Buchungen außerhalb der festgelegten Arbeitszeit erlauben und auf Arbeitszeit anpassen. regex:^(true|false)$
BOOKING_FOR_UNKNOWN_USER=true # Buchungen mit unbekannter CardUID erlauben. regex:^(true|false)$

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
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
test:

234
Readme.md
View File

@@ -2,121 +2,167 @@
[![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
```bash
git clone https://git.letsstein.de/tom/arbeitszeitmessung arbeitszeitmessung
cd arbeitszeitmessung/Docker
# .env Datei anpassen
docker compose up -d
cd arbeitszeitmessung
./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)
## Administration:
Ansicht der Führungskraft (/team):
### Nutzer erstellen:
![team](docs/images/team.png)
Nutzerdaten erstellen:
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
}
]
```sql
INSERT INTO "s_personal_daten"
(
"personal_nummer",
"vorname",
"nachname",
"card_uid",
"geburtsdatum",
"geschlecht",
"adresse",
"plz",
"hauptbeschaeftigungs_ort",
"aktiv_beschaeftigt",
"vorgesetzter_pers_nr",
"arbeitszeit_min_start",
"arbeitszeit_max_ende",
"arbeitszeit_per_tag",
"arbeitszeit_per_woche",
)
VALUES (
1,
'Max',
'Mustermann',
'acde-edca',
'2003-02-01',
1,
'Musterstr. 42',
'00001',
1,
true,
123,
'07:00:00',
'20:00:00',
8,
40
);
```
Antwort `500`
Serverfehler
Nutzerpasswort generieren (kann auch später als Passwort reset genutzt werden):
#### [PUT] Anfrage
Parameter: id (int)
Body: (veränderte Parameter)
```json
{
"cradID": "test_card",
"readerID": "mytest",
"bookingTyp": 1,
"loggedTime": "2024-09-05T08:51:12.670827Z"
}
```sql
INSERT INTO "user_password"
("personal_nummer", "pass_hash")
VALUES (123, crypt('password', gen_salt('bf')));
```
Antwort `200`
### Buchungstypen erstellen:
```json
{
"cradID": "test_card",
"readerID": "mytest",
"bookingTyp": 1,
"loggedTime": "2024-09-05T08:51:12.670827Z",
"id": 6
}
Ohne definierte Anwesenheits und Abwesenheitstypen funktioniert die Anwendung nicht!
Anwesenheiten:
```sql
INSERT INTO "s_anwesenheit_typen"
("anwesenheit_id", "anwesenheit_name")
VALUES (1, 'Büro');
```
### Neue Buchung [/time/new]
Abwesenheiten:
#### [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
}
```sql
INSERT INTO "s_abwesenheit_typen"
("abwesenheit_id", "abwesenheit_name", "arbeitszeit_equivalent")
VALUES (1, 'Urlaub', 100);
```
Antwort `409` Konflikt
Die vorherige Buchung am selben Tag hat den gleichen Buchungstyp
### Feiertage erstellen:
Die gesetzlichen Feiertage für Deutschland/Sachsen werden automatisch mit der Route `auto/feiertage` für das aktuelle Kalenderjahr erzeugt. Um weitere Unternehmensspezifische Feiertage (z.B. 24.12. oder 31.12.) mit in die Liste der Feiertage aufzunehmen, müssen diese manuell erstellt werden.
```sql
INSERT INTO "s_feiertage"
("datum", "name", "arbeitszeit_equivalent", "wiederholen")
VALUES ('2026-12-24', 'Helligabend', 50, 1);
```
Wenn `wiederholen` == 1 wird der Feiertag automatisch beim Aufruf von `auto/feiertage` mit ins nächste Jahr (am selben Datum) übernommen.
Das Feld `arbeitszeit_equivalent` `arbeitszeit_equivalent` ist die prozentuelle Zeit am Tag welche durch diesen Eintrag eingenommen wird. (dies gilt auch für die [Buchungstypen](#buchungstypen-erstellen))
Alle weiteren Tabellen sollte ausschließlich über die Weboberfläche oder per API befüllt werden.
---
# Filestrukture
```
├── Backend (Webserver)
│   ├── doc (Templates for Document Creator --> typst used to create PDF Reports)
│   │   ├── static
│   │   └── templates
│   ├── endpoints (HTML Server endpoints (see main.go for Routes))
│   ├── helper (Helper classes)
│   │   ├── logs
│   │   └── paramParser
│   ├── logs (Log Folder, no sourcecode)
│   ├── migrations (DB Migrations Folder, no direct sourcecode)
│   ├── models (DB Models and their function)
│   ├── src (Tailwind src --> used to config css formatter)
│   ├── static (Webserver static, used to server static content, e.g. JS and CSS files)
│   │   └── css
│   └── 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
```

View File

@@ -1,15 +1,21 @@
#!/usr/bin/env bash
#©Tom Tröger 2026
set -e
envFile=Docker/.env
envBkp=Docker/.env.old
envExample=Docker/env.example
autoBackupScript=Cron/autoBackup.sh
autoHolidaysScript=Cron/autoHolidays.sh
autoLogoutScript=Cron/autoLogout.sh
cronFilePath=Cron
customCronFilePath=Docker/config/cron
echo "Checking Docker installation..."
if ! command -v docker >/dev/null 2>&1; then
autoBackupScript=autoBackup.sh
autoHolidaysScript=autoHolidays.sh
autoLogoutScript=autoLogout.sh
function checkDocker() {
echo "Checking Docker installation..."
if ! command -v docker >/dev/null 2>&1; then
echo "Docker not found. Install Docker? [y/N]"
read -r install_docker
if [[ "$install_docker" =~ ^[Yy]$ ]]; then
@@ -18,42 +24,68 @@ if ! command -v docker >/dev/null 2>&1; then
echo "Docker is required. Exiting."
exit 1
fi
else
else
echo "Docker is already installed."
fi
fi
###########################################################################
###########################################################################
echo "Checking Docker Compose..."
if ! docker compose version >/dev/null 2>&1; then
echo "Checking Docker Compose..."
if ! docker compose version >/dev/null 2>&1; then
echo "Docker Compose plugin missing. You may need to update Docker."
exit 1
fi
fi
}
###########################################################################
echo "Preparing .env file..."
if [ ! -f $envFile ]; then
function setupConfig() {
local reconfig=false
if [ $# -gt 0 ]; then
if ask_reconfig $1 "Reconfigure .env File?"
then
reconfig=true
else
return 0
fi
fi
echo -e "\r\n==================================================\r\n"
echo "Preparing .env file..."
if [ ! -f $envFile ] || [ $reconfig == true ]; then
if [ -f $envExample ]; then
if [ $reconfig == true ]; then
echo "Reconfiguring env file. Backup stored at $envBkp"
echo "All previous values will be used as defaults!"
cp $envFile $envBkp
else
echo ".env not found. Creating interactively from .env.example."
fi
> $envFile
while IFS= read -r line; do
#ignore empty lines and comments
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
key=$(printf "%s" "$line" | cut -d '=' -f 1)
rest=$(printf "%s" "$line" | cut -d '=' -f 2-)
local key=$(printf "%s" "$line" | cut -d '=' -f 1)
local rest=$(printf "%s" "$line" | cut -d '=' -f 2-)
# extract inline comment portion
comment=$(printf "%s" "$rest" | sed -n 's/.*# \(.*\)$/\1/p')
raw_val=$(printf "%s" "$rest" | sed 's/ *#.*//')
default_value=$(printf "%s" "$raw_val" | sed 's/"//g')
local comment=$(printf "%s" "$rest" | sed -n 's/.*# \(.*\)$/\1/p')
local raw_val=$(printf "%s" "$rest" | sed 's/ *#.*//')
local default_value=$(printf "%s" "$raw_val" | sed 's/"//g')
if [ $reconfig == true ]; then
local previous_value=$(grep -E "^$key=" $envBkp | cut -d= -f2)
if [ -n "$previous_value" ]; then
default_value=$previous_value
fi
fi
# Replace __ROOT__ with script pwd
default_value="${default_value/__ROOT__/$(pwd)}"
local default_value="${default_value/__ROOT__/$(pwd)}"
regex=""
if [[ "$comment" =~ regex:(.*)$ ]]; then
@@ -64,9 +96,9 @@ if [ ! -f $envFile ]; then
while true; do
if [ -z "$comment" ]; then
printf "Value for $key - $comment (default: $default_value"
else
printf "Value for $key (default: $default_value"
else
printf "Value for $key - $comment (default: $default_value"
fi
if [ -n "$regex" ]; then
printf ", must match: %s" "$regex"
@@ -103,42 +135,74 @@ if [ ! -f $envFile ]; then
echo "Creating an empty .env file for manual editing."
touch $envFile
fi
else
else
echo "Using existing .env. (found at $envFile)"
fi
fi
}
###########################################################################
LOG_PATH=$(grep -E '^LOG_PATH=' $envFile | cut -d= -f2)
if [ -z "$LOG_PATH" ]; then
function setupFolders(){
if [ $# -gt 0 ]; then
if ! ask_reconfig $1 "Recreate Folders?"
then
return 0
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
else
LOG_PATH=Docker/$LOG_PATH
fi
mkdir -p $LOG_PATH
echo "Created logs folder at $LOG_PATH"
fi
if [ ! -d "$LOG_PATH" ]; then
mkdir -p $LOG_PATH
echo "Created logs folder at $LOG_PATH"
fi
POSTGRES_PATH=$(grep -E '^POSTGRES_PATH=' $envFile | cut -d= -f2)
if [ -z "$POSTGRES_PATH" ]; then
echo "POSTGRES_PATH not found in .env using default $(pwd)/DB"
POSTGRES_PATH=$(pwd)/DB
fi
if [ ! -d "$POSTGRES_PATH" ]; then
mkdir -p $POSTGRES_PATH
echo "Created DB folder at $POSTGRES_PATH"
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
if [ ! -d "$BACKUP_FOLDER" ]; then
mkdir -p $BACKUP_FOLDER
echo "Created backup folder at $BACKUP_FOLDER"
fi
}
###########################################################################
echo -e "\n\n"
echo "Start containers with docker compose up -d? [y/N]"
read -r start_containersmkdi
if [[ "$start_containers" =~ ^[Yy]$ ]]; then
function setupCron(){
echo -e "\r\n==================================================\r\n"
echo "Setup Crontab for automatic logout, backup and holiday creation? [y/N]"
read -r setup_cron
if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
echo "Copying custom cron files to $customCronFilePath"
mkdir -p "$customCronFilePath"
cd Docker
docker compose up -d
echo "Containers started."
else
echo "You can start them manually with: docker compose up -d"
fi
if [ ! -s "$customCronFilePath/$autoBackupScript" ];then
cp "$cronFilePath/$autoBackupScript" "$customCronFilePath/$autoBackupScript"
echo "Copied $autoBackupScript"
fi
###########################################################################
if [ ! -s "$customCronFilePath/$autoLogoutScript" ];then
cp "$cronFilePath/$autoLogoutScript" "$customCronFilePath/$autoLogoutScript"
echo "Copied $autoLogoutScript"
fi
if [ ! -s "$customCronFilePath/$autoHolidaysScript" ];then
cp "$cronFilePath/$autoHolidaysScript" "$customCronFilePath/$autoHolidaysScript"
echo "Copied $autoHolidaysScript"
fi
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"
@@ -147,22 +211,30 @@ if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
POSTGRES_DB=$(grep -E '^POSTGRES_DB=' $envFile | cut -d= -f2)
if [ -z "$POSTGRES_DB" ]; then
echo "arbeitszeitmessung not found in .env using default arbeitszeitmessung"
echo "POSTGRES_DB not found in .env using default arbeitszeitmessung"
POSTGRES_DB="arbeitszeitmessung"
fi
sed -i "s/__PORT__/$WEB_PORT/" $autoHolidaysScript
sed -i "s/__PORT__/$WEB_PORT/" $autoLogoutScript
sed -i "s/__DATABASE__/$POSTGRES_DB/" $autoBackupScript
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
chmod +x $autoBackupScript $autoHolidaysScript $autoLogoutScript
sed -i "s|__PORT__|$WEB_PORT|" $customCronFilePath/$autoHolidaysScript && \
sed -i "s|__PORT__|$WEB_PORT|" $customCronFilePath/$autoLogoutScript && \
sed -i "s|__DATABASE__|$POSTGRES_DB|" $customCronFilePath/$autoBackupScript && \
sed -i "s|__BACKUP_FOLDER__|$BACKUP_FOLDER|" $customCronFilePath/$autoBackupScript
chmod +x "$customCronFilePath/$autoBackupScript" "$customCronFilePath/$autoHolidaysScript" "$customCronFilePath/$autoLogoutScript"
# echo "Scripts build with PORT=$WEB_PORT and DATABSE=$POSTGRES_DB!"
echo "Adding rules to crontab."
cron_commands=$(mktemp /tmp/arbeitszeitmessung-cron.XXX)
pwd
for file in Cron/*; do
for file in $customCronFilePath/*; do
cron_timing=$(grep -E '^# cron-timing:' "$file" | sed 's/^# cron-timing:[[:space:]]*//')
if [ -z "$cron_timing" ]; then
@@ -181,6 +253,75 @@ if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
echo "For how to start a service, see: https://wiki.ubuntuusers.de/systemd/systemctl UNITNAME will be cron.service"
fi
else
else
echo "Please setup cron manually by executing crontab -e and adding all files from inside the Cron directory!"
fi
fi
}
###########################################################################
function startContainer(){
echo -e "\r\n==================================================\r\n"
echo "Start containers with docker compose up -d? [y/N]"
read -r start_containers
if [[ "$start_containers" =~ ^[Yy]$ ]]; then
cd Docker
docker compose up -d
echo "Containers started."
else
echo "You can start them manually with: docker compose up -d"
fi
}
###########################################################################
function help(){
echo "Installer Script für Arbeitszeitmessung Software"
echo -e "\r\n==================================================\r\n"
echo "Nutzung: ./install.sh [options]"
echo -e "\r\n==================================================\r\n"
echo "Optionen:"
echo " -h zeigt diese Übersicht"
echo " -c .env Datei bearbeiten/aktualisieren && cron neu configurieren"
echo -e "\r\n=================================================="
}
###########################################################################
function main(){
echo -e "================Arbeitszeitmessung================\r\n"
if [ $# -gt 0 ];then
if [ $1 == reconfig ]; then
echo -e "================Reconfiguring================\r\n"
setupConfig $1
setupFolders $1
setupCron $1
fi
else
checkDocker
setupConfig
setupFolders
setupCron
startContainer
fi
echo "Installation finished, you can re-run the script any time!"
}
###########################################################################
function ask_reconfig(){
echo -e "\r\n==================================================\r\n"
echo "$2 [y/N]"
read -r do_reconfig
[[ "$do_reconfig" =~ ^[Yy]$ ]] && return # true
echo "Skipping..."
return 1
}
###########################################################################
while getopts ":hc" opt; do
case $opt in
h) help; exit 0 ;;
c) main reconfig; exit 0 ;;
*) echo "Ungültiges Argument"; exit 1 ;;
esac
done
main