53 Commits

Author SHA1 Message Date
tom
0eb4878c90 updated pdf form to send user to pdf generator
All checks were successful
Tests / Run Go Tests (push) Successful in 3m28s
Arbeitszeitmessung Deploy / Build Go Image and Upload (push) Successful in 4m24s
2025-12-12 15:07:57 +01:00
tom
c7f8595474 updated github workflow for document creator
All checks were successful
Tests / Run Go Tests (push) Successful in 1m55s
2025-12-12 14:28:49 +01:00
fcec748293 Merge pull request 'refactor + added kurzarbeit to pdf export' (#66) from dev/broken into dev/pdf
All checks were successful
Tests / Run Go Tests (push) Successful in 1m57s
Reviewed-on: #66
2025-12-12 14:17:16 +01:00
tom
a1b225478a fixed sonarqube issue
All checks were successful
Tests / Run Go Tests (push) Successful in 1m45s
2025-12-12 14:13:24 +01:00
588bf908c6 fixed tests
Some checks failed
Tests / Run Go Tests (push) Failing after 1m37s
2025-12-12 12:58:41 +01:00
76b23133d0 fixed #61, #62 refactored getTime variants
Some checks failed
Tests / Run Go Tests (push) Failing after 1m20s
2025-12-12 12:26:40 +01:00
1ccc19b85c removed and refactored virtual and real worktime 2025-12-12 06:31:03 +01:00
f73c2b1a96 tried to add untertags krank
Some checks failed
Tests / Run Go Tests (push) Failing after 2m3s
2025-12-05 15:03:44 +01:00
a6ea625e8f upped test coverage of helpers
Some checks failed
Tests / Run Go Tests (push) Failing after 1m27s
2025-12-03 00:38:26 +01:00
386f11ec7e fixed #63 all scheduled tasks are now under route /auto and will require api key in the future
Some checks failed
Tests / Run Go Tests (push) Failing after 1m32s
2025-12-03 00:20:11 +01:00
6c0a8bca64 added tests
All checks were successful
Tests / Run Go Tests (push) Successful in 2m5s
2025-12-02 17:10:17 +01:00
6e238c4532 update badge
All checks were successful
Tests / Run Go Tests (push) Successful in 1m28s
2025-12-02 16:57:10 +01:00
74bce88cc0 fixed #60
Some checks failed
Arbeitszeitmessung Deploy / Run Go Tests (push) Successful in 1m29s
Tests / Run Go Tests (push) Has been cancelled
Arbeitszeitmessung Deploy / Build Go Image and Upload (push) Successful in 2m7s
2025-12-02 16:53:43 +01:00
5a5e776e8b fixed #59 2025-12-02 16:53:33 +01:00
02b5d88d34 added typst as doc creator and put it into compose container 2025-12-02 16:50:37 +01:00
6ab48eb534 dev/install (#58)
All checks were successful
Tests / Run Go Tests (push) Successful in 1m24s
improved ci/cd pipeline + interactive install script

Reviewed-on: #58
Co-authored-by: Tom Tröger <t.troeger.02@gmail.com>
Co-committed-by: Tom Tröger <t.troeger.02@gmail.com>
2025-11-30 21:25:47 +01:00
7e5eaebca9 added worktime base to work time calculations
Some checks failed
Tests / Run Go Tests (push) Failing after 1m4s
2025-10-31 23:57:42 +01:00
ac59d2642f fixed sonarqube issues
All checks were successful
Tests / Run Go Tests (push) Successful in 1m34s
2025-10-29 00:17:07 +01:00
cf5238f024 seperated pdf-generate endpoint + added new helper in Time 2025-10-28 22:59:33 +01:00
4bc5594dc5 build uppon paramParser 2025-10-28 22:59:09 +01:00
a634b7a69e separaded endpoints + cleaned page templates + added constants to time formatting
Some checks failed
Tests / Run Go Tests (push) Failing after 1m34s
2025-10-27 22:53:07 +01:00
e1f0f85401 small refactor of sonarqube issue
Some checks failed
Tests / Run Go Tests (push) Failing after 1m37s
2025-10-24 12:20:05 +02:00
b6644f3584 added pdf generation with typst, working on pdf input form 2025-10-24 00:20:51 +02:00
tom
7eda8eb538 reworked pdf exporter to use typst
Some checks failed
Tests / Run Go Tests (push) Failing after 1m44s
2025-10-23 16:18:22 +02:00
0d7696cbc6 adding more logging + working on displaying if a workday was submitted
Some checks failed
Tests / Run Go Tests (push) Failing after 1m55s
2025-10-14 01:05:02 +02:00
tom
5001f24d9b implemented log levels and structured log with slog
Some checks failed
Tests / Run Go Tests (push) Failing after 1m36s
2025-10-13 22:33:48 +02:00
ea8e78fd9f fixed #56
Some checks failed
Tests / Run Go Tests (push) Failing after 1m22s
2025-10-09 16:42:49 +02:00
6da58d6753 fixed #54, #55
All checks were successful
Arbeitszeitmessung Deploy / Run Go Tests (push) Successful in 1m28s
Tests / Run Go Tests (push) Successful in 2m19s
Arbeitszeitmessung Deploy / Build Go Image and Upload (push) Successful in 1m39s
2025-10-09 13:12:07 +02:00
tom
89eb5d255d fixed #52
Some checks failed
Tests / Run Go Tests (push) Failing after 1m23s
2025-10-08 12:59:47 +02:00
1b8fb747e8 Update Readme.md
Some checks failed
Tests / Run Go Tests (push) Failing after 22s
2025-10-07 16:24:33 +02:00
74cded42d8 Update test.yaml
All checks were successful
Tests / Run Go Tests (push) Successful in 1m21s
2025-10-07 16:23:01 +02:00
22350142fc Update test.yaml
All checks were successful
Tests / Run Go Tests (push) Successful in 1m21s
2025-10-07 16:20:23 +02:00
659fb80049 updated test.yml
Some checks failed
Tests / Run Go Tests (push) Failing after 1m25s
2025-10-07 16:14:09 +02:00
cbc4028f8d moved sonar.properties
Some checks failed
Tests / Run Go Tests (push) Failing after 1m20s
2025-10-07 16:11:17 +02:00
e4d423385a Update test.yaml
Some checks failed
Tests / Run Go Tests (push) Failing after 1m0s
2025-10-07 16:07:11 +02:00
c9c2d801b0 Update test.yaml
Some checks failed
Tests / Run Go Tests (push) Has been cancelled
2025-10-07 16:06:49 +02:00
94c7c8a36e Update sonar-project.properties
All checks were successful
Tests / Run Go Tests (push) Successful in 1m23s
2025-10-07 15:59:31 +02:00
d69ec600cd Check for coverage output
All checks were successful
Tests / Run Go Tests (push) Successful in 1m23s
2025-10-07 15:56:49 +02:00
95d5c4ab9d Update script.js
All checks were successful
Tests / Run Go Tests (push) Successful in 1m21s
2025-10-07 15:50:29 +02:00
bf841ad5c6 updated sonarqube + fixed first issues
Some checks failed
Tests / Run Go Tests (push) Failing after 1m26s
2025-10-07 15:47:59 +02:00
a1aae9dc56 working on sonarqube
All checks were successful
Tests / Run Go Tests (push) Successful in 1m23s
2025-10-07 15:08:40 +02:00
750fb1ff58 Update test.yaml
All checks were successful
Tests / Run Go Tests (push) Successful in 1m17s
2025-10-07 15:00:39 +02:00
f4e9915e7f Update test.yaml
Some checks failed
Tests / Run Go Tests (push) Failing after 50s
2025-10-07 14:58:53 +02:00
18046bbe18 added sonarqube for static code analysis
All checks were successful
Tests / Run Go Tests (push) Successful in 1m25s
2025-10-07 14:54:31 +02:00
75929e3b7d Merge pull request 'fixed #50, added default action input to defaultDayComponent' (#51) from dev/ui into main
All checks were successful
Tests / Run Go Tests (push) Successful in 49s
Arbeitszeitmessung Deploy / Run Go Tests (push) Successful in 54s
Arbeitszeitmessung Deploy / Build Go Image and Upload (push) Successful in 1m9s
Reviewed-on: #51
2025-10-07 13:01:24 +02:00
627f5b7e5b fixed #50, added default action input to defaultDayComponent
All checks were successful
Tests / Run Go Tests (push) Successful in 22s
2025-10-07 12:55:47 +02:00
9e5dc760d5 Merge pull request 'UX/UI Impovements' (#48) from dev/ui into main
All checks were successful
Arbeitszeitmessung Deploy / Run Go Tests (push) Successful in 52s
Tests / Run Go Tests (push) Successful in 50s
Arbeitszeitmessung Deploy / Build Go Image and Upload (push) Successful in 2m33s
Reviewed-on: #48
2025-10-04 19:39:11 +02:00
tom
0ffb910e37 added user informations cell
All checks were successful
Tests / Run Go Tests (push) Successful in 16s
2025-10-04 19:37:57 +02:00
tom
566776910a ui/ux improvements on time page
All checks were successful
Tests / Run Go Tests (push) Successful in 15s
2025-10-04 19:16:21 +02:00
4d00143a74 Merge pull request 'UI Changes' (#47) from dev/ui into main
All checks were successful
Tests / Run Go Tests (push) Successful in 16s
Reviewed-on: #47
2025-10-01 23:12:56 +02:00
c093127a8c added worktime + overtime to pdf
All checks were successful
Tests / Run Go Tests (push) Successful in 1m48s
2025-10-01 23:02:57 +02:00
3dd4b134c8 closes #44
All checks were successful
Tests / Run Go Tests (push) Successful in 1m43s
2025-10-01 22:53:27 +02:00
5fbe53faf6 Merge pull request 'kurzarbeit + multi day absence' (#46) from dev/kurzarbeit into main
All checks were successful
Tests / Run Go Tests (push) Successful in 1m17s
Arbeitszeitmessung Deploy / Run Go Tests (push) Successful in 1m20s
Arbeitszeitmessung Deploy / Build Go Image and Upload (push) Successful in 1m25s
Reviewed-on: #46
2025-09-30 00:18:51 +02:00
73 changed files with 3201 additions and 1466 deletions

View File

@@ -2,56 +2,15 @@ name: Arbeitszeitmessung Deploy
run-name: ${{ gitea.actor }} is building and deploying arbeitszeitmesssung run-name: ${{ gitea.actor }} is building and deploying arbeitszeitmesssung
on: on:
push: push:
tags: "*" tags:
- "*"
branches:
- main
jobs: jobs:
testing: webserver:
name: Run Go Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: password
POSTGRES_DB: arbeitszeitmessung
env:
POSTGRES_HOST: postgres
POSTGRES_USER: root
POSTGRES_PASSWORD: password
POSTGRES_DB: arbeitszeitmessung
POSTGRES_PORT: 5432
RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup go
uses: actions/setup-go@v5
with:
go-version-file: Backend/go.mod
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
id: hash-go
with:
patterns: |
go.mod
go.sum
- name: cache go
id: cache-go
uses: actions/cache@v4
with:
path: |-
/go_path
/go_cache
key: arbeitszeitmessung-${{ steps.hash-go.outputs.hash }}
restore-keys: |-
arbeitszeitmessung-
- name: Run Go Tests
run: cd Backend && go test ./...
build:
name: Build Go Image and Upload name: Build Go Image and Upload
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [testing]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -65,12 +24,49 @@ jobs:
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: git.letsstein.de/tom/arbeitszeitmessung-webserver
tags: |
type=raw,value=latest
type=pep440,pattern={{version}}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
context: Backend context: Backend
tags: ${{ steps.meta.outputs.tags }}
document-creator:
name: Build Go Image and Upload
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: git.letsstein.de
username: ${{ gitea.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: git.letsstein.de/tom/arbeitszeitmessung-doc-creator
tags: | tags: |
git.letsstein.de/tom/arbeitszeitmessung:latest type=raw,value=latest
git.letsstein.de/tom/arbeitszeitmessung:${{ github.ref_name }} type=pep440,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
context: Backend
tags: ${{ steps.meta.outputs.tags }}

View File

@@ -15,15 +15,17 @@ jobs:
POSTGRES_DB: arbeitszeitmessung POSTGRES_DB: arbeitszeitmessung
env: env:
POSTGRES_HOST: postgres POSTGRES_HOST: postgres
POSTGRES_USER: root POSTGRES_API_USER: root
POSTGRES_PASSWORD: password POSTGRES_API_PASS: password
POSTGRES_DB: arbeitszeitmessung POSTGRES_DB: arbeitszeitmessung
POSTGRES_PORT: 5432 POSTGRES_PORT: 5432
RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
# Disabling shallow clone is recommended for improving relevancy of reporting
fetch-depth: 0
- name: Setup go - name: Setup go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
@@ -45,4 +47,25 @@ jobs:
restore-keys: |- restore-keys: |-
arbeitszeitmessung- arbeitszeitmessung-
- name: Run Go Tests - name: Run Go Tests
run: cd Backend && go test ./... run: cd Backend && mkdir .test && go test ./... -coverprofile=.test/coverage.out -json > .test/report.json
- name: Verify coverage report exists
run: |
if [ -f "Backend/.test/coverage.out" ]; then
echo "Coverage report found"
else
echo "Coverage report not found"
fi
- uses: SonarSource/sonarqube-scan-action@v6
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
with:
projectBaseDir: Backend
args: >
-Dsonar.projectVersion=${{ gitea.sha_short }}
- uses: SonarSource/sonarqube-quality-gate-action@v1
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
scanMetadataReportFile: Backend/.scannerwork/report-task.txt

1
.gitignore vendored
View File

@@ -39,3 +39,4 @@ node_modules
atlas.hcl atlas.hcl
.scannerwork .scannerwork
Backend/logs Backend/logs
.worktime.txt

View File

@@ -13,7 +13,7 @@ RUN go mod download && go mod verify
COPY . . COPY . .
RUN go build -o server . RUN go build -o server .
FROM alpine FROM alpine:3.22
RUN apk add --no-cache tzdata RUN apk add --no-cache tzdata
WORKDIR /app WORKDIR /app
COPY --from=build /app/server /app/server COPY --from=build /app/server /app/server

View File

@@ -14,7 +14,8 @@ func OpenDatabase() (models.IDatabase, error) {
dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung") dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung")
dbUser := helper.GetEnv("POSTGRES_API_USER", "api_nutzer") dbUser := helper.GetEnv("POSTGRES_API_USER", "api_nutzer")
dbPassword := helper.GetEnv("POSTGRES_API_PASS", "password") 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=Europe/Berlin", dbUser, dbPassword, dbHost, dbName) connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=%s", dbUser, dbPassword, dbHost, dbName, dbTz)
return sql.Open("postgres", connStr) return sql.Open("postgres", connStr)
} }

View File

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

View File

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

View File

@@ -0,0 +1,222 @@
package endpoints
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser"
"arbeitszeitmessung/models"
"bytes"
"fmt"
"log"
"log/slog"
"net/http"
"time"
"github.com/Dadido3/go-typst"
)
const DE_DATE string = "02.01.2006"
func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) {
var typstDays []typstDay
for _, day := range days {
var thisTypstDay typstDay
work, pause, overtime := day.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
if day.IsWorkDay() {
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")
typstDayPart.BookingTo = workDay.Bookings[i+1].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,
})
}
if workdayAbsence := workDay.GetWorktimeAbsence(); (workdayAbsence != models.Absence{}) {
typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: workdayAbsence.AbwesenheitTyp.Name})
}
} else {
absentDay, _ := day.(*models.Absence)
typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name})
}
return typstDayParts
}
func PDFCreateController(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r)
switch r.Method {
case http.MethodGet:
user, err := models.GetUserFromSession(Session, r.Context())
if err != nil {
log.Println("Error getting user!")
return
}
pp := paramParser.New(r.URL.Query())
startDate := pp.ParseTimestampFallback("start_date", time.DateOnly, time.Now())
personalNumbers := pp.ParseIntListFallback("employe_list", ",", make([]int, 0))
employes, err := models.GetUserByPersonalNrMulti(personalNumbers)
if err != nil {
slog.Warn("Error getting employes!", slog.Any("Error", err))
return
}
output, err := createReports(user, employes, startDate)
if err != nil {
slog.Warn("Could not create pdf report", slog.Any("Error", err))
}
switch pp.ParseStringFallback("output", "render") {
case "render":
w.Header().Set("Content-type", "application/pdf")
output.WriteTo(w)
w.WriteHeader(http.StatusOK)
case "download":
panic("Not implemented")
}
default:
http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed)
}
}
func createReports(user models.User, employes []models.User, startDate time.Time) (bytes.Buffer, error) {
startDate = helper.GetFirstOfMonth(startDate)
endDate := startDate.AddDate(0, 1, -1)
var employeData []typstData
for _, employe := range employes {
if data, err := createEmployeReport(employe, startDate, endDate); err != nil {
slog.Warn("Error when creating employeReport", slog.Any("user", employe), slog.Any("error", err))
} else {
employeData = append(employeData, data)
}
}
return renderPDF(employeData)
}
func createEmployeReport(employee models.User, startDate, endDate time.Time) (typstData, error) {
targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate))
workDaysThisMonth := models.GetDays(employee, startDate, endDate.AddDate(0, 0, 1), false)
slog.Debug("Baseline Working hours", "targetHours", targetHoursThisMonth.Hours())
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)
if err != nil {
slog.Warn("Failed to convert to days", slog.Any("error", err))
return typstData{}, 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),
WorkTime: helper.FormatDurationFill(workHours, true),
Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true),
OvertimeTotal: "",
CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"),
}
return typstData{Meta: metadata, Days: typstDays}, nil
}
func renderPDF(data []typstData) (bytes.Buffer, error) {
var markup bytes.Buffer
var output bytes.Buffer
if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil {
return output, err
}
// Import the template and invoke the template function with the custom data.
// Show is used to replace the current document with whatever content the template function in `template.typ` returns.
markup.WriteString(`
#import "templates/abrechnung.typ": abrechnung
#for d in data {
abrechnung(d.Meta, d.Days)
}
`)
// Compile the prepared markup with Typst and write the result it into `output.pdf`.
typstCLI := typst.DockerExec{
ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
}
if err := typstCLI.Compile(&markup, &output, nil); err != nil {
return output, err
}
return output, nil
}
type typstMetadata struct {
TimeRange string `json:"time-range"`
EmployeeName string `json:"employee-name"`
WorkTime string `json:"worktime"`
Kurzarbeit string `json:"kurzarbeit"`
Overtime string `json:"overtime"`
OvertimeTotal string `json:"overtime-total"`
CurrentTimestamp string `json:"current-timestamp"`
}
type typstDayPart struct {
BookingFrom string `json:"booking-from"`
BookingTo string `json:"booking-to"`
WorkType string `json:"worktype"`
IsWorkDay bool `json:"is-workday"`
}
type typstDay struct {
Date string `json:"date"`
DayParts []typstDayPart `json:"day-parts"`
Worktime string `json:"worktime"`
Pausetime string `json:"pausetime"`
Overtime string `json:"overtime"`
Kurzarbeit string `json:"kurzarbeit"`
IsFriday bool `json:"is-weekend"`
}
type typstData struct {
Meta typstMetadata `json:"meta"`
Days []typstDay `json:"days"`
}

View File

@@ -4,33 +4,22 @@ import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"arbeitszeitmessung/templates" "arbeitszeitmessung/templates"
"log" "log/slog"
"net/http" "net/http"
"time"
) )
func PDFHandler(w http.ResponseWriter, r *http.Request) { func PDFFormHandler(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r) helper.RequiresLogin(Session, w, r)
startDate, err := parseTimestamp(r, "start", time.Now().Format("2006-01-02"))
if err != nil {
log.Println("Error parsing 'start_date' time", err)
http.Error(w, "Timestamp 'start_date' cannot be parsed!", http.StatusBadRequest)
return
}
if startDate.Day() > 1 {
startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1))
}
endDate := startDate.AddDate(0, 1, -1)
user, err := models.GetUserFromSession(Session, r.Context()) user, err := models.GetUserFromSession(Session, r.Context())
if err != nil { if err != nil {
log.Println("Error getting user!") slog.Warn("Error getting user!", slog.Any("Error", err))
// TODO add error handling
} }
//TODO: only accepted weeks teamMembers, err := user.GetTeamMembers()
if err != nil {
weeks := models.GetDays(user, startDate, endDate, false) slog.Warn("Error getting team members!", slog.Any("Error", err))
}
// log.Printf("Using Dates: %s - %s\n", startDate.String(), endDate.String()) templates.PDFForm(teamMembers).Render(r.Context(), w)
templates.PDFReportEmploye(user, weeks, startDate, endDate).Render(r.Context(), w)
} }

View File

@@ -28,10 +28,9 @@ func teamPresence(w http.ResponseWriter, r *http.Request) {
log.Println("Error getting user!", err) log.Println("Error getting user!", err)
} }
team, err := user.GetTeamMembers() team, err := user.GetTeamMembers()
teamPresence := make(map[bool][]models.User) teamPresence := make(map[models.User]bool)
for _, user := range team { for _, user := range team {
present := user.CheckAnwesenheit() teamPresence[user] = user.CheckAnwesenheit()
teamPresence[present] = append(teamPresence[present], user)
} }
if err != nil { if err != nil {

View File

@@ -2,13 +2,13 @@ package endpoints
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"arbeitszeitmessung/templates" "arbeitszeitmessung/templates"
"context" "context"
"errors" "errors"
"log" "log"
"net/http" "net/http"
"strconv"
"time" "time"
) )
@@ -30,17 +30,17 @@ func submitReport(w http.ResponseWriter, r *http.Request) {
log.Println("Error parsing form", err) log.Println("Error parsing form", err)
return return
} }
userPN, _ := strconv.Atoi(r.FormValue("user")) pp := paramParser.New(r.Form)
_weekTs := r.FormValue("week") userPN, err := pp.ParseInt("user")
weekTs, err := time.Parse(time.DateOnly, _weekTs) weekTs := pp.ParseTimestampFallback("week", time.DateOnly, time.Now())
user, err := models.GetUserByPersonalNr(userPN) user, err := models.GetUserByPersonalNr(userPN)
workWeek := models.NewWorkWeek(user, weekTs, true)
if err != nil { if err != nil {
log.Println("Could not get user!") log.Println("Could not get user!")
return return
} }
workWeek := models.NewWorkWeek(user, weekTs, true)
switch r.FormValue("method") { switch r.FormValue("method") {
case "send": case "send":
err = workWeek.SendWeek() err = workWeek.SendWeek()
@@ -62,14 +62,11 @@ func showWeeks(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/user/login", http.StatusSeeOther) http.Redirect(w, r, "/user/login", http.StatusSeeOther)
return return
} }
submissionDate := r.URL.Query().Get("submission_date")
lastSub := user.GetLastWorkWeekSubmission() pp := paramParser.New(r.URL.Query())
if submissionDate != "" { submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission())
submissionDate, err := time.Parse("2006-01-02", submissionDate) lastSub := helper.GetMonday(submissionDate)
if err == nil {
lastSub = helper.GetMonday(submissionDate)
}
}
userWeek := models.NewWorkWeek(user, lastSub, true) userWeek := models.NewWorkWeek(user, lastSub, true)
var workWeeks []models.WorkWeek var workWeeks []models.WorkWeek

View File

@@ -2,14 +2,15 @@ package endpoints
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"arbeitszeitmessung/templates" "arbeitszeitmessung/templates"
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"log" "log"
"log/slog"
"net/http" "net/http"
"sort"
"strconv" "strconv"
"time" "time"
) )
@@ -52,7 +53,7 @@ func parseTimestamp(r *http.Request, getKey string, fallback string) (time.Time,
if getTimestamp == "" { if getTimestamp == "" {
getTimestamp = fallback getTimestamp = fallback
} }
Timestamp, err := time.Parse("2006-01-02", getTimestamp) Timestamp, err := time.Parse(time.DateOnly, getTimestamp)
if err != nil { if err != nil {
return time.Now(), err return time.Now(), err
} }
@@ -67,26 +68,15 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/user/login", http.StatusSeeOther) http.Redirect(w, r, "/user/login", http.StatusSeeOther)
return return
} }
pp := paramParser.New(r.URL.Query())
// TODO add config for timeoffset // TODO add config for timeoffset
tsFrom, err := parseTimestamp(r, "time_from", time.Now().AddDate(0, -1, 0).Format("2006-01-02")) tsFrom := pp.ParseTimestampFallback("time_from", time.DateOnly, time.Now().AddDate(0, -1, 0))
if err != nil { tsTo := pp.ParseTimestampFallback("time_to", time.DateOnly, time.Now())
log.Println("Error parsing 'from' time", err)
http.Error(w, "Timestamp 'from' cannot be parsed!", http.StatusBadRequest)
return
}
tsTo, err := parseTimestamp(r, "time_to", time.Now().Format("2006-01-02"))
if err != nil {
log.Println("Error parsing 'to' time", err)
http.Error(w, "Timestamp 'to' cannot be parsed!", http.StatusBadRequest)
return
}
tsTo = tsTo.AddDate(0, 0, 1) // so that today is inside tsTo = tsTo.AddDate(0, 0, 1) // so that today is inside
days := models.GetDays(user, tsFrom, tsTo, true) days := models.GetDays(user, tsFrom, tsTo, true)
sort.Slice(days, func(i, j int) bool {
return days[i].Date().After(days[j].Date())
})
lastSub := user.GetLastWorkWeekSubmission() lastSub := user.GetLastWorkWeekSubmission()
var aggregatedOvertime time.Duration var aggregatedOvertime time.Duration
@@ -94,7 +84,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
if day.Date().Before(lastSub) { if day.Date().Before(lastSub) {
continue continue
} }
aggregatedOvertime += day.TimeOvertimeReal(user) aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, false)
} }
if reportedOvertime, err := user.GetReportedOvertime(); err == nil { if reportedOvertime, err := user.GetReportedOvertime(); err == nil {
user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute) user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute)
@@ -116,6 +106,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
func updateBooking(w http.ResponseWriter, r *http.Request) { func updateBooking(w http.ResponseWriter, r *http.Request) {
r.ParseForm() r.ParseForm()
pp := paramParser.New(r.Form)
var loc *time.Location var loc *time.Location
loc, err := time.LoadLocation(helper.GetEnv("TZ", "Europe/Berlin")) loc, err := time.LoadLocation(helper.GetEnv("TZ", "Europe/Berlin"))
if err != nil { if err != nil {
@@ -127,6 +118,7 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
log.Println("No user found!", err) log.Println("No user found!", err)
return return
} }
switch r.FormValue("action") { switch r.FormValue("action") {
case "add": case "add":
timestamp, err := time.ParseInLocation("2006-01-02|15:04", r.FormValue("date")+"|"+r.FormValue("timestamp"), loc) timestamp, err := time.ParseInLocation("2006-01-02|15:04", r.FormValue("date")+"|"+r.FormValue("timestamp"), loc)
@@ -135,10 +127,9 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
return return
} }
var check_in_out int check_in_out, err := pp.ParseInt("check_in_out")
check_in_out, err = strconv.Atoi(r.FormValue("check_in_out"))
if err != nil { if err != nil {
log.Println("Error parsing check_in_out", err) slog.Warn("Error parsing check_in_out")
return return
} }
@@ -149,14 +140,14 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
log.Printf("Error inserting booking %v -> %v\n", newBooking, err) log.Printf("Error inserting booking %v -> %v\n", newBooking, err)
} }
case "change": case "change":
absenceType, err := strconv.Atoi(r.FormValue("absence")) // absenceType, err := strconv.Atoi(r.FormValue("absence"))
if err != nil { // if err != nil {
log.Println("Error parsing absence type.", err) // log.Println("Error parsing absence type.", err)
absenceType = 0 // absenceType = 0
} // }
if absenceType != 0 { // if absenceType != 0 {
createAbsence(absenceType, user, loc, r) // createAbsence(absenceType, user, loc, r)
} // }
for index, possibleBooking := range r.PostForm { for index, possibleBooking := range r.PostForm {
if len(index) > 7 && index[:7] == "booking" { if len(index) > 7 && index[:7] == "booking" {
booking_id, err := strconv.Atoi(index[8:]) booking_id, err := strconv.Atoi(index[8:])
@@ -174,10 +165,11 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
log.Println("Error parsing time!", err) log.Println("Error parsing time!", err)
continue continue
} }
// log.Println("Parsing time", parsedTime)
booking.UpdateTime(parsedTime) booking.UpdateTime(parsedTime)
} }
} }
default:
log.Println("No action from /time found")
} }
getBookings(w, r) getBookings(w, r)
} }
@@ -191,13 +183,13 @@ func updateAbsence(r *http.Request) error {
loc = time.Local loc = time.Local
} }
dateFrom, err := time.ParseInLocation("2006-01-02", r.FormValue("date_from"), loc) dateFrom, err := time.ParseInLocation(time.DateOnly, r.FormValue("date_from"), loc)
if err != nil { if err != nil {
log.Println("Error parsing date_from input for absence", err) log.Println("Error parsing date_from input for absence", err)
return err return err
} }
dateTo, err := time.ParseInLocation("2006-01-02", r.FormValue("date_to"), loc) dateTo, err := time.ParseInLocation(time.DateOnly, r.FormValue("date_to"), loc)
if err != nil { if err != nil {
log.Println("Error parsing date_to input for absence", err) log.Println("Error parsing date_to input for absence", err)
return err return err
@@ -259,22 +251,3 @@ func updateAbsence(r *http.Request) error {
return nil return nil
} }
func createAbsence(absenceType int, user models.User, loc *time.Location, r *http.Request) {
absenceDate, err := time.ParseInLocation("2006-01-02", r.FormValue("date"), loc)
if err != nil {
log.Println("Cannot get date from input! Skipping absence creation", err)
return
}
absence, err := models.NewAbsence(user.CardUID, absenceType, absenceDate)
if err != nil {
log.Println("Error creating absence!", err)
return
}
err = absence.Insert()
if err != nil {
log.Println("Error inserting absence!", err)
return
}
}

View File

@@ -3,6 +3,7 @@ package endpoints
import ( import (
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"arbeitszeitmessung/templates" "arbeitszeitmessung/templates"
"context"
"log" "log"
"net/http" "net/http"
) )
@@ -38,5 +39,9 @@ func changePassword(w http.ResponseWriter, r *http.Request) {
} }
func showUserPage(w http.ResponseWriter, r *http.Request, status int) { func showUserPage(w http.ResponseWriter, r *http.Request, status int) {
templates.UserPage(status).Render(r.Context(), w) var ctx context.Context
if user, err := models.GetUserFromSession(Session, r.Context()); err == nil {
ctx = context.WithValue(r.Context(), "user", user)
}
templates.SettingsPage(status).Render(ctx, w)
} }

View File

@@ -29,6 +29,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
func UserSettingsHandler(w http.ResponseWriter, r *http.Request) { func UserSettingsHandler(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r) helper.RequiresLogin(Session, w, r)
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
showUserPage(w, r, 0) showUserPage(w, r, 0)

View File

@@ -9,6 +9,7 @@ require github.com/a-h/templ v0.3.943
require github.com/alexedwards/scs/v2 v2.8.0 require github.com/alexedwards/scs/v2 v2.8.0
require ( require (
github.com/Dadido3/go-typst v0.8.0
github.com/golang-migrate/migrate/v4 v4.18.3 github.com/golang-migrate/migrate/v4 v4.18.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
) )
@@ -16,6 +17,14 @@ require (
require ( require (
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/smasher164/xid v0.1.2 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.38.0 // indirect
) )
tool golang.org/x/tools/cmd/deadcode

View File

@@ -1,5 +1,7 @@
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Dadido3/go-typst v0.8.0 h1:uTLYprhkrBjwsCXRRuyYUFL0fpYHa2kIYoOB/CGqVNs=
github.com/Dadido3/go-typst v0.8.0/go.mod h1:QYis9sT70u65kn1SkFfyPRmHsPxgoxWbAixwfPReOZA=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY= github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY=
@@ -54,6 +56,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smasher164/xid v0.1.2 h1:erplXSdBRIIw+MrwjJ/m8sLN2XY16UGzpTA0E2Ru6HA=
github.com/smasher164/xid v0.1.2/go.mod h1:tgivm8CQl19fH1c5y+8F4mA+qY6n2i6qDRBlY/6nm+I=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
@@ -68,7 +72,19 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -14,7 +14,7 @@ type FileLog struct {
var Logs map[string]FileLog = make(map[string]FileLog) var Logs map[string]FileLog = make(map[string]FileLog)
func NewAudit() (i *log.Logger, close func() error) { func NewAudit() (i *log.Logger, close func() error) {
LOG_FILE := "logs/" + time.Now().Format("2006-01-02") + ".log" 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) logFile, err := os.OpenFile(LOG_FILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)

View File

@@ -0,0 +1,118 @@
package paramParser
import (
"fmt"
"log/slog"
"net/url"
"strconv"
"strings"
"time"
)
type ParamsParser struct {
urlParams url.Values
}
func (p ParamsParser) ParseStringListFallback(key string, delimiter string, fallback []string) []string {
if !p.urlParams.Has(key) {
return fallback
}
paramList := p.urlParams.Get(key)
list := strings.Split(paramList, delimiter)
return list
}
func (p ParamsParser) ParseIntListFallback(key string, delimiter string, fallback []int) []int {
if !p.urlParams.Has(key) {
return fallback
}
paramList := p.urlParams.Get(key)
list := strings.Split(paramList, delimiter)
parsedList := make([]int, 0)
for _, item := range list {
if parsedItem, err := strconv.Atoi(item); err == nil {
parsedList = append(parsedList, parsedItem)
}
}
return parsedList
}
type NoValueError struct {
Key string
}
func (e *NoValueError) Error() string {
return fmt.Sprintf("No value found for key %s", e.Key)
}
func New(params url.Values) ParamsParser {
return ParamsParser{
urlParams: params,
}
}
func (p *ParamsParser) ParseTimestampFallback(key string, format string, fallback time.Time) time.Time {
if !p.urlParams.Has(key) {
return fallback
}
paramTimestamp := p.urlParams.Get(key)
if timestamp, err := time.Parse(format, paramTimestamp); err == nil {
return timestamp
} else {
slog.Warn("Error parsing HTTP Params to time.Time", slog.Any("key", key), slog.Any("error", err))
return fallback
}
}
func (p *ParamsParser) ParseTimestamp(key string, format string) (time.Time, error) {
if !p.urlParams.Has(key) {
return time.Time{}, &NoValueError{Key: key}
}
paramTimestamp := p.urlParams.Get(key)
if timestamp, err := time.Parse(format, paramTimestamp); err == nil {
return timestamp, nil
} else {
slog.Debug("Error parsing HTTP Params to time.Time", slog.Any("key", key), slog.Any("error", err))
return timestamp, err
}
}
func (p *ParamsParser) ParseStringFallback(key string, fallback string) string {
if !p.urlParams.Has(key) {
return fallback
}
return p.urlParams.Get(key)
}
func (p *ParamsParser) ParseString(key string) (string, error) {
if !p.urlParams.Has(key) {
return "", &NoValueError{Key: key}
}
return p.urlParams.Get(key), nil
}
func (p *ParamsParser) ParseIntFallback(key string, fallback int) int {
if !p.urlParams.Has(key) {
return fallback
}
paramInt := p.urlParams.Get(key)
if result, err := strconv.Atoi(paramInt); err == nil {
return result
} else {
slog.Warn("Error parsing HTTP Params to Int", slog.Any("key", key), slog.Any("error", err))
return fallback
}
}
func (p *ParamsParser) ParseInt(key string) (int, error) {
if !p.urlParams.Has(key) {
return 0, &NoValueError{Key: key}
}
paramInt := p.urlParams.Get(key)
if result, err := strconv.Atoi(paramInt); err == nil {
return result, nil
} else {
slog.Debug("Error parsing HTTP Params to Int", slog.Any("key", key), slog.Any("error", err))
return 0, err
}
}

View File

@@ -0,0 +1,47 @@
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

@@ -16,6 +16,13 @@ func GetMonday(ts time.Time) time.Time {
return ts return ts
} }
func GetFirstOfMonth(ts time.Time) time.Time {
if ts.Day() > 1 {
return ts.AddDate(0, 0, -(ts.Day() - 1))
}
return ts
}
func IsWeekend(ts time.Time) bool { func IsWeekend(ts time.Time) bool {
return ts.Weekday() == time.Saturday || ts.Weekday() == time.Sunday return ts.Weekday() == time.Saturday || ts.Weekday() == time.Sunday
} }
@@ -55,3 +62,23 @@ func FormatDurationFill(d time.Duration, fill bool) string {
func IsSameDate(a, b time.Time) bool { func IsSameDate(a, b time.Time) bool {
return a.Truncate(24 * time.Hour).Equal(b.Truncate(24 * time.Hour)) return a.Truncate(24 * time.Hour).Equal(b.Truncate(24 * time.Hour))
} }
func GetWorkingDays(startDate, endDate time.Time) int {
if endDate.Before(startDate) {
return 0
}
var count int = 0
for d := startDate.Truncate(24 * time.Hour); !d.After(endDate); d = d.Add(24 * time.Hour) {
if !IsWeekend(d) {
count++
}
}
return count
}
func FormatGermanDayOfWeek(t time.Time) string {
return days[t.Weekday()][:2]
}
var days = [...]string{
"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"}

View File

@@ -1,37 +1,172 @@
package helper package helper
import ( import (
"fmt"
"testing" "testing"
"time" "time"
) )
func TestGetMonday(t *testing.T) { func TestGetMonday(t *testing.T) {
isMonday, err := time.Parse("2006-01-02", "2025-07-14") isMonday, err := time.Parse(time.DateOnly, "2025-07-14")
notMonday, err := time.Parse("2006-01-02", "2025-07-16") isSunday, err := time.Parse(time.DateOnly, "2025-07-20")
notMonday, err := time.Parse(time.DateOnly, "2025-07-16")
if err != nil || isMonday.Equal(notMonday) { if err != nil || isMonday.Equal(notMonday) {
t.Errorf("U stupid? %e", err) t.Errorf("U stupid? %e", err)
} }
if GetMonday(isMonday) != isMonday || GetMonday(notMonday) != isMonday { if GetMonday(isMonday) != isMonday {
t.Error("Wrong date conversion!") t.Error("Wrong date conversion!")
} }
if GetMonday(notMonday) != isMonday {
t.Error("Wrong date conversion (notMonday)!")
}
if GetMonday(isSunday) != isMonday {
t.Error("Wrong date conversion (isSunday)!")
}
} }
func TestFormatDuration(t *testing.T) { func TestFormatDurationFill(t *testing.T) {
durations := []struct { testCases := []struct {
name string name string
duration time.Duration duration time.Duration
fill bool
}{ }{
{"2h", time.Duration(120 * time.Minute)}, {"2h", time.Duration(120 * time.Minute), true},
{"30min", time.Duration(30 * time.Minute)}, {"30min", time.Duration(30 * time.Minute), true},
{"1h 30min", time.Duration(90 * time.Minute)}, {"1h 30min", time.Duration(90 * time.Minute), true},
{"-1h 30min", time.Duration(-90 * time.Minute)}, {"-1h 30min", time.Duration(-90 * time.Minute), true},
{"", 0}, {"0min", 0, true},
{"", 0, false},
} }
for _, d := range durations { for _, tc := range testCases {
t.Run(d.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
if FormatDuration(d.duration) != d.name { if FormatDurationFill(tc.duration, tc.fill) != tc.name {
t.Error("Format missmatch in Formatduration.") t.Error("Format missmatch in Formatduration.")
} }
}) })
} }
} }
func TestFormatDuration(t *testing.T) {
testCases := []struct {
name string
duration time.Duration
}{
{"", 0},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if FormatDuration(tc.duration) != tc.name {
t.Error("Format missmatch in Formatduration.")
}
})
}
}
func TestIsSameDate(t *testing.T) {
testCases := []struct {
dateA string
dateB string
result bool
}{
{"2025-12-01 00:00:00", "2025-12-01 00:00:00", true},
{"2025-12-03 00:00:00", "2025-12-02 00:00:00", false},
{"2025-12-03 23:45:00", "2025-12-03 00:00:00", true},
{"2025-12-04 24:12:00", "2025-12-04 00:12:00", false},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("IsSameDateTest: %s date", tc.dateA), func(t *testing.T) {
dateA, _ := time.Parse(time.DateTime, tc.dateA)
dateB, _ := time.Parse(time.DateTime, tc.dateB)
if IsSameDate(dateA, dateB) != tc.result {
t.Errorf("Is SameDate did not match! Result %t", IsSameDate(dateA, dateB))
}
})
}
}
func TestGetWorkingDays(t *testing.T) {
testCases := []struct {
start string
end string
days int
}{
{"2025-10-01", "2025-10-02", 2},
{"2025-10-02", "2025-10-01", 0},
{"2025-10-01", "2025-10-31", 23},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("WorkingDayTest: %d days", tc.days), func(t *testing.T) {
startDate, _ := time.Parse(time.DateOnly, tc.start)
endDate, _ := time.Parse(time.DateOnly, tc.end)
if GetWorkingDays(startDate, endDate) != tc.days {
t.Error("Calculated workdays do not match target")
}
})
}
}
func TestFormatGermanDayOfWeek(t *testing.T) {
testCases := []struct {
date string
result string
}{
{"2025-12-01", "Mo"},
{"2025-12-02", "Di"},
{"2025-12-03", "Mi"},
{"2025-12-04", "Do"},
{"2025-12-05", "Fr"},
{"2025-12-06", "Sa"},
{"2025-12-07", "So"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("FormatWeekDayTest: %s date", tc.date), func(t *testing.T) {
date, _ := time.Parse(time.DateOnly, tc.date)
if FormatGermanDayOfWeek(date) != tc.result {
t.Error("Formatted workday did not match!")
}
})
}
}
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

@@ -7,3 +7,19 @@ type TimeFormValue struct {
TsTo time.Time TsTo time.Time
CardUID string CardUID string
} }
func BoolToInt(b bool) int {
var i int = 0
if b {
i = 1
}
return i
}
func BoolToInt8(b bool) int8 {
var i int8 = 0
if b {
i = 1
}
return i
}

View File

@@ -0,0 +1,26 @@
package helper
import (
"fmt"
"testing"
)
func TestBoolToInt(t *testing.T) {
testCases := []struct {
value bool
res int
res8 int8
}{
{true, 1, 1},
{false, 0, 0},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("BoolToInt value: %t", tc.value), func(t *testing.T) {
if BoolToInt(tc.value) != tc.res || BoolToInt8(tc.value) != tc.res8 {
t.Error("How could you... mess up bool to int")
}
})
}
}

112
Backend/helper/web_test.go Normal file
View File

@@ -0,0 +1,112 @@
package helper
import (
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/alexedwards/scs/v2"
)
func TestSetCors_WhenNoCorsTrue(t *testing.T) {
os.Setenv("NO_CORS", "true")
defer os.Unsetenv("NO_CORS")
rr := httptest.NewRecorder()
SetCors(rr)
h := rr.Header()
if h.Get("Access-Control-Allow-Origin") != "*" {
t.Errorf("expected Access-Control-Allow-Origin to be '*', got %q", h.Get("Access-Control-Allow-Origin"))
}
if h.Get("Access-Control-Allow-Methods") != "*" {
t.Errorf("expected Access-Control-Allow-Methods to be '*', got %q", h.Get("Access-Control-Allow-Methods"))
}
if h.Get("Access-Control-Allow-Headers") != "*" {
t.Errorf("expected Access-Control-Allow-Headers to be '*', got %q", h.Get("Access-Control-Allow-Headers"))
}
}
func TestSetCors_WhenNoCorsFalse(t *testing.T) {
os.Setenv("NO_CORS", "false")
defer os.Unsetenv("NO_CORS")
rr := httptest.NewRecorder()
SetCors(rr)
h := rr.Header()
if h.Get("Access-Control-Allow-Origin") != "" ||
h.Get("Access-Control-Allow-Methods") != "" ||
h.Get("Access-Control-Allow-Headers") != "" {
t.Errorf("CORS headers should not be set when NO_CORS=false")
}
}
func TestRequiresLogin_DebugMode_NoRedirect(t *testing.T) {
os.Setenv("GO_ENV", "debug")
defer os.Unsetenv("GO_ENV")
session := scs.New()
req := httptest.NewRequest("GET", "/", nil)
rr := httptest.NewRecorder()
RequiresLogin(session, rr, req)
if rr.Result().StatusCode == http.StatusSeeOther {
t.Errorf("expected no redirect in debug mode")
}
}
// func TestRequiresLogin_UserExists_NoRedirect(t *testing.T) {
// os.Setenv("GO_ENV", "production")
// defer os.Unsetenv("GO_ENV")
// session := scs.New()
// req := httptest.NewRequest("GET", "/", nil)
// ctx, err := session.Load(req.Context(), "")
// if err != nil {
// t.Fatalf("session load error: %v", err)
// }
// ctx = session.Put(ctx, "user", "123")
// req = req.WithContext(context.WithValue(ctx, "session", session))
// rr := httptest.NewRecorder()
// yourpkg.RequiresLogin(session, rr, req)
// if rr.Result().StatusCode == http.StatusSeeOther {
// t.Errorf("expected no redirect when user exists")
// }
// }
// func TestRequiresLogin_NoUser_Redirects(t *testing.T) {
// os.Setenv("GO_ENV", "production")
// defer os.Unsetenv("GO_ENV")
// session := scs.New()
// req := httptest.NewRequest("GET", "/", nil)
// req = req.WithContext(context.WithValue(req.Context(), "session", session))
// rr := httptest.NewRecorder()
// RequiresLogin(session, rr, req)
// if rr.Result().StatusCode != http.StatusSeeOther {
// t.Errorf("expected redirect when user does not exist, got %d", rr.Result().StatusCode)
// }
// location := rr.Result().Header.Get("Location")
// if location != "/user/login" {
// t.Errorf("expected redirect to /user/login, got %q", location)
// }
// }

View File

@@ -5,8 +5,7 @@ import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"context" "context"
"fmt" "log/slog"
"log"
"net/http" "net/http"
"os" "os"
"time" "time"
@@ -17,23 +16,25 @@ import (
func main() { func main() {
var err error var err error
var logLevel slog.LevelVar
logLevel.Set(slog.LevelWarn)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &logLevel}))
slog.SetDefault(logger)
err = godotenv.Load(".env") err = godotenv.Load(".env")
if err != nil { if err != nil {
log.Println("No .env file found in directory!") slog.Info("No .env file found in directory!")
} }
if helper.GetEnv("GO_ENV", "production") == "debug" { if helper.GetEnv("GO_ENV", "production") == "debug" {
log.Println("Debug mode enabled") logLevel.Set(slog.LevelDebug)
log.Println("Environment Variables")
envs := os.Environ() envs := os.Environ()
for _, e := range envs { slog.Debug("Debug mode enabled", "Environment Variables", envs)
fmt.Println(e)
}
} }
models.DB, err = OpenDatabase() models.DB, err = OpenDatabase()
if err != nil { if err != nil {
log.Fatal(err) slog.Error("Error while opening the database", "Error", err)
} }
fs := http.FileServer(http.Dir("./static")) fs := http.FileServer(http.Dir("./static"))
@@ -45,27 +46,30 @@ func main() {
server.HandleFunc("/time/new", endpoints.TimeCreateHandler) server.HandleFunc("/time/new", endpoints.TimeCreateHandler)
server.Handle("/absence", ParamsMiddleware(endpoints.AbsencHandler)) server.Handle("/absence", ParamsMiddleware(endpoints.AbsencHandler))
server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler)) server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler))
server.HandleFunc("/logout", endpoints.LogoutHandler) server.HandleFunc("/auto/logout", endpoints.LogoutHandler)
server.HandleFunc("/auto/kurzarbeit", endpoints.KurzarbeitFillHandler)
server.HandleFunc("/user/{action}", endpoints.UserHandler) server.HandleFunc("/user/{action}", endpoints.UserHandler)
// server.HandleFunc("/user/login", endpoints.LoginHandler) // server.HandleFunc("/user/login", endpoints.LoginHandler)
// server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) // server.HandleFunc("/user/settings", endpoints.UserSettingsHandler)
server.HandleFunc("/team", endpoints.TeamHandler) server.HandleFunc("/team", endpoints.TeamHandler)
server.HandleFunc("/team/presence", endpoints.TeamPresenceHandler) server.HandleFunc("/presence", endpoints.TeamPresenceHandler)
server.HandleFunc("/pdf", endpoints.PDFHandler) server.Handle("/pdf", ParamsMiddleware(endpoints.PDFFormHandler))
server.HandleFunc("/pdf/generate", endpoints.PDFCreateController)
server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect)) server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect))
server.Handle("/static/", http.StripPrefix("/static/", fs)) server.Handle("/static/", http.StripPrefix("/static/", fs))
serverSessionMiddleware := endpoints.Session.LoadAndSave(server) serverSessionMiddleware := endpoints.Session.LoadAndSave(server)
// starting the http server // starting the http server
fmt.Printf("Server is running at http://localhost:%s\n", helper.GetEnv("EXPOSED_PORT", "8080")) slog.Info("Server is running at http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", serverSessionMiddleware)) slog.Error("Error starting Server", "Error", http.ListenAndServe(":8080", serverSessionMiddleware))
} }
func ParamsMiddleware(next http.HandlerFunc) http.Handler { func ParamsMiddleware(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query() queryParams := r.URL.Query()
ctx := context.WithValue(r.Context(), "urlParams", queryParams) ctx := context.WithValue(r.Context(), "urlParams", queryParams)
slog.Debug("ParamsMiddleware added urlParams", slog.Any("urlParams", queryParams))
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
}) })
} }

View File

@@ -49,26 +49,45 @@ func (a *Absence) IsMultiDay() bool {
return !a.DateFrom.Equal(a.DateTo) return !a.DateFrom.Equal(a.DateTo)
} }
func (a *Absence) TimeWorkVirtual(u User) time.Duration { func (a *Absence) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
return a.TimeWorkReal(u) switch base {
case WorktimeBaseDay:
if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit {
return u.ArbeitszeitProTagFrac(1)
} else if a.AbwesenheitTyp.WorkTime <= 0 {
return 0
} }
return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100)
func (a *Absence) TimeWorkReal(u User) time.Duration { case WorktimeBaseWeek:
if a.AbwesenheitTyp.WorkTime > 1 { if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit {
return time.Duration(u.ArbeitszeitPerTag * float32(time.Hour)).Round(time.Minute) return u.ArbeitszeitProTagFrac(0.2)
} else if a.AbwesenheitTyp.WorkTime <= 0 {
return 0
}
return u.ArbeitszeitProWocheFrac(0.2 * float32(a.AbwesenheitTyp.WorkTime) / 100)
} }
return 0 return 0
} }
func (a *Absence) TimePauseReal(u User) (work, pause time.Duration) { func (a *Absence) GetPausetime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
return 0, 0
}
func (a *Absence) TimeOvertimeReal(u User) time.Duration {
if a.AbwesenheitTyp.WorkTime > 1 {
return 0 return 0
} }
return -u.ArbeitszeitProTag()
func (a *Absence) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
if a.AbwesenheitTyp.WorkTime > 0 {
return 0
}
switch base {
case WorktimeBaseDay:
return -u.ArbeitszeitProTagFrac(1)
case WorktimeBaseWeek:
return -u.ArbeitszeitProWocheFrac(0.2)
}
return 0
}
func (a *Absence) GetTimes(u User, base WorktimeBase, includeKurzarbeit bool) (work, pause, overtime time.Duration) {
return a.GetWorktime(u, base, includeKurzarbeit), a.GetPausetime(u, base, includeKurzarbeit), a.GetOvertime(u, base, includeKurzarbeit)
} }
func (a *Absence) ToString() string { func (a *Absence) ToString() string {

View File

@@ -0,0 +1,92 @@
package models_test
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"testing"
"time"
)
var testAbsence = models.Absence{
Day: CatchError(time.Parse(time.DateOnly, "2025-01-01")),
AbwesenheitTyp: models.AbsenceType{},
DateFrom: CatchError(time.Parse(time.DateOnly, "2025-01-01")),
DateTo: CatchError(time.Parse(time.DateOnly, "2025-01-03")),
}
var testKurzarbeit = models.AbsenceType{
Name: "Kurzarbeit",
WorkTime: -1,
}
var testUrlaub = models.AbsenceType{
Name: "Urlaub",
WorkTime: 100,
}
var testUrlaubUntertags = models.AbsenceType{
Name: "Urlaub untertags",
WorkTime: 50,
}
func TestCalcRealWorkTimeDayAbsence(t *testing.T) {
testCases := []struct {
absenceType models.AbsenceType
expectedTime time.Duration
}{
{
absenceType: testUrlaub,
expectedTime: time.Hour * 8,
},
{
absenceType: testUrlaubUntertags,
expectedTime: time.Hour * 4,
},
{
absenceType: testKurzarbeit,
expectedTime: 0,
},
}
for _, tc := range testCases {
t.Run("Calc Absence Worktime: "+tc.absenceType.Name, func(t *testing.T) {
var testCase = testAbsence
testCase.AbwesenheitTyp = tc.absenceType
workTime := testCase.GetWorktime(testUser, models.WorktimeBaseDay, false)
if workTime != tc.expectedTime {
t.Errorf("Calc Worktime Default not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true))
}
})
}
}
func TestCalcRealWorkTimeWeekAbsence(t *testing.T) {
testCases := []struct {
absenceType models.AbsenceType
expectedTime time.Duration
}{
{
absenceType: testUrlaub,
expectedTime: time.Hour * 7,
},
{
absenceType: testUrlaubUntertags,
expectedTime: time.Hour*3 + time.Minute*30,
},
{
absenceType: testKurzarbeit,
expectedTime: 0,
},
}
for _, tc := range testCases {
t.Run("Calc Absence Worktime: "+tc.absenceType.Name, func(t *testing.T) {
var testCase = testAbsence
testCase.AbwesenheitTyp = tc.absenceType
workTime := testCase.GetWorktime(testUser, models.WorktimeBaseWeek, false)
if workTime != tc.expectedTime {
t.Errorf("Calc Worktime Default not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true))
}
})
}
}

View File

@@ -6,6 +6,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"log" "log"
"log/slog"
"net/url" "net/url"
"strconv" "strconv"
"time" "time"
@@ -87,6 +88,27 @@ func (b *Booking) Verify() bool {
return true return true
} }
func (b *Booking) IsSubmittedAndChecked() bool {
qStr, err := DB.Prepare(`SELECT bestaetigt from wochen_report WHERE $1 = ANY(anwesenheiten);`)
if err != nil {
slog.Warn("Error when preparing SQL Statement", "error", err)
return false
}
defer qStr.Close()
var isSubmittedAndChecked bool = false
err = qStr.QueryRow(b.CounterId).Scan(&isSubmittedAndChecked)
if err == sql.ErrNoRows {
// No rows found ==> not even submitted
return false
}
if err != nil {
slog.Warn("Unexpected error when executing SQL Statement", "error", err)
}
return isSubmittedAndChecked
}
func (b *Booking) Insert() error { func (b *Booking) Insert() error {
if !checkLastBooking(*b) { if !checkLastBooking(*b) {
return SameBookingError{} return SameBookingError{}

View File

@@ -10,36 +10,36 @@ var testBookingType = models.BookingType{
Name: "Büro", Name: "Büro",
} }
var testBookings8hrs = []models.Booking{models.Booking{ var testBookings8hrs = []models.Booking{{
CardUID: "aaaa-aaaa", CardUID: "aaaa-aaaa",
CheckInOut: 1, CheckInOut: 1,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")), Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")),
BookingType: testBookingType, BookingType: testBookingType,
}, models.Booking{ }, {
CardUID: "aaaa-aaaa", CardUID: "aaaa-aaaa",
CheckInOut: 2, CheckInOut: 2,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 16:00")), Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 16:00")),
BookingType: testBookingType, BookingType: testBookingType,
}} }}
var testBookings6hrs = []models.Booking{models.Booking{ var testBookings6hrs = []models.Booking{{
CardUID: "aaaa-aaaa", CardUID: "aaaa-aaaa",
CheckInOut: 1, CheckInOut: 1,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")), Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")),
BookingType: testBookingType, BookingType: testBookingType,
}, models.Booking{ }, {
CardUID: "aaaa-aaaa", CardUID: "aaaa-aaaa",
CheckInOut: 2, CheckInOut: 2,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 14:00")), Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 14:00")),
BookingType: testBookingType, BookingType: testBookingType,
}} }}
var testBookings10hrs = []models.Booking{models.Booking{ var testBookings10hrs = []models.Booking{{
CardUID: "aaaa-aaaa", CardUID: "aaaa-aaaa",
CheckInOut: 1, CheckInOut: 1,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")), Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")),
BookingType: testBookingType, BookingType: testBookingType,
}, models.Booking{ }, {
CardUID: "aaaa-aaaa", CardUID: "aaaa-aaaa",
CheckInOut: 2, CheckInOut: 2,
Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 18:00")), Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 18:00")),

View File

@@ -0,0 +1,55 @@
package models
import (
"arbeitszeitmessung/helper"
"log"
"time"
)
type IWorkDay interface {
Date() time.Time
ToString() string
IsWorkDay() bool
IsKurzArbeit() bool
GetDayProgress(User) int8
RequiresAction() bool
GetWorktime(User, WorktimeBase, bool) time.Duration
GetPausetime(User, WorktimeBase, bool) time.Duration
GetTimes(User, WorktimeBase, bool) (work, pause, overtime time.Duration)
GetOvertime(User, WorktimeBase, bool) time.Duration
}
func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay {
var allDays map[string]IWorkDay = make(map[string]IWorkDay)
for _, day := range GetWorkDays(user, tsFrom, tsTo) {
allDays[day.Date().Format(time.DateOnly)] = &day
}
absences, err := GetAbsencesByCardUID(user.CardUID, tsFrom, tsTo)
if err != nil {
log.Println("Error gettings absences for all Days!", err)
return nil
}
for _, day := range absences {
if helper.IsWeekend(day.Date()) {
continue
}
// Absence should be integrated in workday
switch {
case day.AbwesenheitTyp.WorkTime < 0:
if workDay, ok := allDays[day.Date().Format(time.DateOnly)].(*WorkDay); ok {
workDay.kurzArbeit = true
workDay.kurzArbeitAbsence = day
}
case day.AbwesenheitTyp.WorkTime < 100:
if workDay, ok := allDays[day.Date().Format(time.DateOnly)].(*WorkDay); ok {
workDay.worktimeAbsece = day
}
default:
allDays[day.Date().Format(time.DateOnly)] = &day
}
}
sortedDays := sortDays(allDays, orderedForward)
return sortedDays
}

View File

@@ -7,9 +7,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"log/slog"
"time" "time"
"github.com/alexedwards/scs/v2" "github.com/alexedwards/scs/v2"
"github.com/lib/pq"
) )
type User struct { type User struct {
@@ -57,11 +59,37 @@ func (u *User) GetReportedOvertime() (time.Duration, error) {
return overtime, nil return overtime, nil
} }
func GetAllUsers() ([]User, error) {
qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname,arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten;`))
var users []User
if err != nil {
return users, err
}
defer qStr.Close()
rows, err := qStr.Query()
if err != nil {
return users, err
}
defer rows.Close()
for rows.Next() {
var user User
if err := rows.Scan(&user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche); err != nil {
log.Println("Error creating user!", err)
continue
}
users = append(users, user)
}
if err = rows.Err(); err != nil {
return users, nil
}
return users, nil
}
func (u *User) GetAll() ([]User, error) { func (u *User) GetAll() ([]User, error) {
qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname FROM s_personal_daten;`)) qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname FROM s_personal_daten;`))
var users []User var users []User
if err != nil { if err != nil {
fmt.Printf("Error preparing query statement %v\n", err)
return users, err return users, err
} }
defer qStr.Close() defer qStr.Close()
@@ -85,13 +113,21 @@ func (u *User) GetAll() ([]User, error) {
return users, nil return users, nil
} }
// Returns the worktime per day rounded to minutes
func (u *User) ArbeitszeitProTag() time.Duration { func (u *User) ArbeitszeitProTag() time.Duration {
return time.Duration(u.ArbeitszeitPerTag * float32(time.Hour)).Round(time.Minute) return u.ArbeitszeitProTagFrac(1)
}
// Returns the worktime per day rounded to minutes
func (u *User) ArbeitszeitProTagFrac(fraction float32) time.Duration {
return time.Duration(u.ArbeitszeitPerTag * float32(time.Hour) * fraction).Round(time.Minute)
} }
func (u *User) ArbeitszeitProWoche() time.Duration { func (u *User) ArbeitszeitProWoche() time.Duration {
return time.Duration(u.ArbeitszeitPerWoche * float32(time.Hour)).Round(time.Minute) return u.ArbeitszeitProWocheFrac(1)
}
func (u *User) ArbeitszeitProWocheFrac(fraction float32) time.Duration {
return time.Duration(u.ArbeitszeitPerWoche * float32(time.Hour) * fraction).Round(time.Minute)
} }
// Returns true if there is a booking 1 for today -> meaning the user is at work // Returns true if there is a booking 1 for today -> meaning the user is at work
@@ -99,7 +135,7 @@ func (u *User) ArbeitszeitProWoche() time.Duration {
func (u *User) CheckAnwesenheit() bool { func (u *User) CheckAnwesenheit() bool {
qStr, err := DB.Prepare((`SELECT check_in_out FROM anwesenheit WHERE card_uid = $1 AND "timestamp"::date = now()::date ORDER BY "timestamp" DESC LIMIT 1;`)) qStr, err := DB.Prepare((`SELECT check_in_out FROM anwesenheit WHERE card_uid = $1 AND "timestamp"::date = now()::date ORDER BY "timestamp" DESC LIMIT 1;`))
if err != nil { if err != nil {
fmt.Printf("Error preparing query statement %v\n", err) slog.Debug("Error preparing query statement.", "error", err)
return false return false
} }
defer qStr.Close() defer qStr.Close()
@@ -137,11 +173,43 @@ func GetUserByPersonalNr(personalNummer int) (User, error) {
return user, nil return user, nil
} }
func GetUserByPersonalNrMulti(personalNummerMulti []int) ([]User, error) {
var users []User
if len(personalNummerMulti) == 0 {
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[]);`))
if err != nil {
return users, err
}
rows, err := qStr.Query(pq.Array(personalNummerMulti))
if err == sql.ErrNoRows {
return users, err
}
if err != nil {
return nil, err
}
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 {
return users, err
}
users = append(users, user)
}
if err = rows.Err(); err != nil {
return users, err
}
return users, nil
}
func (u *User) Login(password string) bool { func (u *User) Login(password string) bool {
var loginSuccess bool var loginSuccess bool
qStr, err := DB.Prepare((`SELECT (pass_hash = crypt($2, pass_hash)) AS pass_hash FROM user_password WHERE personal_nummer = $1;`)) qStr, err := DB.Prepare((`SELECT (pass_hash = crypt($2, pass_hash)) AS pass_hash FROM user_password WHERE personal_nummer = $1;`))
if err != nil { if err != nil {
log.Println("Error preparing db statement", err) slog.Debug("Error preparing query statement.", "error", err)
return false return false
} }
defer qStr.Close() defer qStr.Close()
@@ -173,7 +241,7 @@ func (u *User) ChangePass(password, newPassword string) (bool, error) {
func (u *User) GetTeamMembers() ([]User, error) { func (u *User) GetTeamMembers() ([]User, error) {
var teamMembers []User var teamMembers []User
qStr, err := DB.Prepare(`SELECT personal_nummer FROM s_personal_daten WHERE vorgesetzter_pers_nr = $1`) qStr, err := DB.Prepare(`SELECT personal_nummer FROM s_personal_daten WHERE vorgesetzter_pers_nr = $1 ORDER BY "nachname";`)
if err != nil { if err != nil {
return teamMembers, err return teamMembers, err
} }
@@ -233,7 +301,7 @@ func (u *User) GetLastWorkWeekSubmission() time.Time {
) AS letzte_buchung; ) AS letzte_buchung;
`) `)
if err != nil { if err != nil {
log.Println("Error preparing statement!", err) slog.Debug("Error preparing query statement.", "error", err)
return lastSub return lastSub
} }
err = qStr.QueryRow(u.PersonalNummer, u.CardUID).Scan(&lastSub) err = qStr.QueryRow(u.PersonalNummer, u.CardUID).Scan(&lastSub)

View File

@@ -6,7 +6,7 @@ import (
"testing" "testing"
) )
var testUser models.User = models.User{Vorname: "Kim", Name: "Mustermensch", PersonalNummer: 456, CardUID: "aaaa-aaaa", ArbeitszeitPerTag: 8, ArbeitszeitPerWoche: 40} var testUser models.User = models.User{Vorname: "Kim", Name: "Mustermensch", PersonalNummer: 456, CardUID: "aaaa-aaaa", ArbeitszeitPerTag: 8, ArbeitszeitPerWoche: 35}
func SetupUserFixture(t *testing.T, db models.IDatabase) { func SetupUserFixture(t *testing.T, db models.IDatabase) {
t.Helper() t.Helper()

View File

@@ -5,66 +5,112 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"log/slog"
"sort" "sort"
"time" "time"
) )
type IWorkDay interface {
Date() time.Time
TimeWorkVirtual(User) time.Duration
TimeWorkReal(User) time.Duration
TimePauseReal(User) (work, pause time.Duration)
TimeOvertimeReal(User) time.Duration
GetAllWorkTimesVirtual(User) (work, pause, overtime time.Duration)
ToString() string
IsWorkDay() bool
IsKurzArbeit() bool
GetDayProgress(User) int8
RequiresAction() bool
}
type WorkDay struct { type WorkDay struct {
Day time.Time `json:"day"` Day time.Time `json:"day"`
Bookings []Booking `json:"bookings"` Bookings []Booking `json:"bookings"`
workTime time.Duration workTime time.Duration
pauseTime time.Duration pauseTime time.Duration
realWorkTime time.Duration
realPauseTime time.Duration
TimeFrom time.Time TimeFrom time.Time
TimeTo time.Time TimeTo time.Time
kurzArbeit bool kurzArbeit bool
kurzArbeitAbsence Absence kurzArbeitAbsence Absence
// Urlaub untertags
worktimeAbsece Absence
} }
func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay { type WorktimeBase string
var allDays map[string]IWorkDay = make(map[string]IWorkDay)
var sortedDays []IWorkDay const (
for _, day := range GetWorkDays(user, tsFrom, tsTo) { WorktimeBaseWeek WorktimeBase = "week"
allDays[day.Date().Format("2006-01-02")] = &day WorktimeBaseDay WorktimeBase = "day"
)
func (d *WorkDay) GetWorktimeAbsence() Absence {
return d.worktimeAbsece
} }
absences, err := GetAbsencesByCardUID(user.CardUID, tsFrom, tsTo)
if err != nil { // Gets the time as is in the db (with corrected pause times)
log.Println("Error gettings absences for all Days!", err) func (d *WorkDay) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
return sortedDays if includeKurzarbeit && d.IsKurzArbeit() && len(d.Bookings) > 0 {
return d.kurzArbeitAbsence.GetWorktime(u, base, true)
} }
for _, day := range absences { work, pause := calcWorkPause(d.Bookings)
if helper.IsWeekend(day.Date()) { work, pause = correctWorkPause(work, pause)
continue if (d.worktimeAbsece != Absence{}) {
work += d.worktimeAbsece.GetWorktime(u, base, false)
} }
if day.AbwesenheitTyp.WorkTime == 1 { return work.Round(time.Minute)
if workDay, ok := allDays[day.Date().Format("2006-01-02")].(*WorkDay); ok { }
workDay.kurzArbeit = true
workDay.kurzArbeitAbsence = day // 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 = correctWorkPause(work, pause)
return pause.Round(time.Minute)
}
// Returns the overtime based on the db entries
func (d *WorkDay) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
work := d.GetWorktime(u, base, includeKurzarbeit)
var targetHours time.Duration
switch base {
case WorktimeBaseDay:
targetHours = u.ArbeitszeitProTag()
case WorktimeBaseWeek:
targetHours = u.ArbeitszeitProWocheFrac(0.2)
}
return (work - targetHours).Round(time.Minute)
}
func (d *WorkDay) GetTimes(u User, base WorktimeBase, includeKurzarbeit bool) (work, pause, overtime time.Duration) {
return d.GetWorktime(u, base, includeKurzarbeit), d.GetPausetime(u, base, includeKurzarbeit), d.GetOvertime(u, base, includeKurzarbeit)
}
func calcWorkPause(bookings []Booking) (work, pause time.Duration) {
var lastBooking Booking
for _, b := range bookings {
if b.CheckInOut%2 == 1 {
if !lastBooking.Timestamp.IsZero() {
pause += b.Timestamp.Sub(lastBooking.Timestamp)
} }
} else { } else {
allDays[day.Date().Format("2006-01-02")] = &day work += b.Timestamp.Sub(lastBooking.Timestamp)
} }
lastBooking = b
}
if len(bookings)%2 == 1 {
work += time.Since(lastBooking.Timestamp.Local())
}
return work, pause
} }
for _, day := range allDays { func correctWorkPause(workIn, pauseIn time.Duration) (work, pause time.Duration) {
if workIn <= 6*time.Hour || pauseIn > 45*time.Minute {
return workIn, pauseIn
}
var diff time.Duration
if workIn <= (9*time.Hour) && pauseIn < 30*time.Minute {
diff = 30*time.Minute - pauseIn
} else if pauseIn < 45*time.Minute {
diff = 45*time.Minute - pauseIn
}
work = workIn - diff
pause = pauseIn + diff
return work, pause
}
func sortDays(days map[string]IWorkDay, forward bool) []IWorkDay {
var sortedDays []IWorkDay
for _, day := range days {
sortedDays = append(sortedDays, day) sortedDays = append(sortedDays, day)
} }
if orderedForward { if forward {
sort.Slice(sortedDays, func(i, j int) bool { sort.Slice(sortedDays, func(i, j int) bool {
return sortedDays[i].Date().After(sortedDays[j].Date()) return sortedDays[i].Date().After(sortedDays[j].Date())
}) })
@@ -80,71 +126,25 @@ func (d *WorkDay) Date() time.Time {
return d.Day return d.Day
} }
func (d *WorkDay) TimeWorkVirtual(u User) time.Duration { func (d *WorkDay) GenerateKurzArbeitBookings(u User) (time.Time, time.Time) {
if d.IsKurzArbeit() { var timeFrom, timeTo time.Time
return u.ArbeitszeitProTag() if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() {
return timeFrom, timeTo
} }
return d.workTime
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())
return timeFrom, timeTo
} }
func (d *WorkDay) GetKurzArbeit() *Absence { func (d *WorkDay) GetKurzArbeit() *Absence {
return &d.kurzArbeitAbsence return &d.kurzArbeitAbsence
} }
func (d *WorkDay) TimeWorkReal(u User) time.Duration {
d.realWorkTime, d.realPauseTime = 0, 0
var lastBooking Booking
for _, booking := range d.Bookings {
if booking.CheckInOut%2 == 1 {
if !lastBooking.Timestamp.IsZero() {
d.realPauseTime += booking.Timestamp.Sub(lastBooking.Timestamp)
}
} else {
d.realWorkTime += booking.Timestamp.Sub(lastBooking.Timestamp)
}
lastBooking = booking
}
if helper.IsSameDate(d.Date(), time.Now()) && len(d.Bookings)%2 == 1 {
d.realWorkTime += time.Since(lastBooking.Timestamp.Local())
}
return d.realWorkTime
}
func (d *WorkDay) TimeOvertimeReal(u User) time.Duration {
workTime := d.TimeWorkVirtual(u)
if workTime == 0 {
workTime, _ = d.TimePauseReal(u)
}
if helper.IsWeekend(d.Day) && len(d.Bookings) == 0 {
return 0
}
var overtime time.Duration
overtime = workTime - u.ArbeitszeitProTag()
return overtime
}
func (d *WorkDay) TimePauseReal(u User) (work, pause time.Duration) {
if d.realWorkTime == 0 {
d.TimeWorkReal(u)
}
d.workTime, d.pauseTime = d.realWorkTime, d.realPauseTime
if d.realWorkTime <= 6*time.Hour || d.realPauseTime > 45*time.Minute {
return d.realWorkTime, d.realPauseTime
}
if d.realWorkTime <= (9*time.Hour) && d.realPauseTime < 30*time.Minute {
diff := 30*time.Minute - d.pauseTime
d.workTime -= diff
d.pauseTime += diff
} else if d.realPauseTime < 45*time.Minute {
diff := 45*time.Minute - d.pauseTime
d.workTime -= diff
d.pauseTime += diff
}
return d.workTime, d.pauseTime
}
func (d *WorkDay) ToString() string { func (d *WorkDay) ToString() string {
return fmt.Sprintf("WorkDay: %s with %d bookings and worktime: %s", d.Date().Format("2006-01-02"), len(d.Bookings), helper.FormatDuration(d.workTime)) return fmt.Sprintf("WorkDay: %s with %d bookings and worktime: %s", d.Date().Format(time.DateOnly), len(d.Bookings), helper.FormatDuration(d.workTime))
} }
func (d *WorkDay) IsWorkDay() bool { func (d *WorkDay) IsWorkDay() bool {
@@ -231,7 +231,6 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
return workDays return workDays
} }
defer rows.Close() defer rows.Close()
// emptyDays, _ := strconv.ParseBool(helper.GetEnv("EMPTY_DAYS", "false"))
for rows.Next() { for rows.Next() {
var workDay WorkDay var workDay WorkDay
var bookings []byte var bookings []byte
@@ -250,7 +249,6 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 { if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 {
workDay.Bookings = []Booking{} workDay.Bookings = []Booking{}
} }
workDay.TimePauseReal(user)
if len(workDay.Bookings) > 1 || !helper.IsWeekend(workDay.Date()) { if len(workDay.Bookings) > 1 || !helper.IsWeekend(workDay.Date()) {
workDays = append(workDays, workDay) workDays = append(workDays, workDay)
} }
@@ -262,18 +260,6 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
return workDays return workDays
} }
func (d *WorkDay) GetAllWorkTimesReal(user User) (work, pause, overtime time.Duration) {
if d.pauseTime == 0 || d.workTime == 0 {
d.TimePauseReal(user)
}
return d.workTime.Round(time.Minute), d.pauseTime.Round(time.Minute), d.TimeOvertimeReal(user)
}
func (d *WorkDay) GetAllWorkTimesVirtual(user User) (work, pause, overtime time.Duration) {
_, pause, overtime = d.GetAllWorkTimesReal(user)
return d.TimeWorkVirtual(user), pause, overtime
}
// returns bool wheter the workday was ended with an automatic logout // returns bool wheter the workday was ended with an automatic logout
func (d *WorkDay) RequiresAction() bool { func (d *WorkDay) RequiresAction() bool {
if len(d.Bookings) == 0 { if len(d.Bookings) == 0 {
@@ -286,19 +272,7 @@ func (d *WorkDay) GetDayProgress(u User) int8 {
if d.RequiresAction() { if d.RequiresAction() {
return -1 return -1
} }
workTime := d.TimeWorkVirtual(u) workTime := d.GetWorktime(u, WorktimeBaseDay, true)
progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100 progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100
return int8(progress) return int8(progress)
} }
// func (d *WorkDay) CalcOvertime(user User) time.Duration {
// if d.workTime == 0 {
// d.TimePauseReal(user)
// }
// if helper.IsWeekend(d.Day) && len(d.Bookings) == 0 {
// return 0
// }
// var overtime time.Duration
// overtime = d.workTime - user.ArbeitszeitProTag()
// return overtime
// }

View File

@@ -16,65 +16,147 @@ func CatchError[T any](val T, err error) T {
} }
var testWorkDay = models.WorkDay{ var testWorkDay = models.WorkDay{
Day: CatchError(time.Parse("2006-01-02", "2025-01-01")), Day: CatchError(time.Parse(time.DateOnly, "2025-01-01")),
Bookings: testBookings8hrs, Bookings: testBookings8hrs,
TimeFrom: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")), 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")), TimeTo: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 16:30")),
} }
func TestCalcRealWorkTime(t *testing.T) { func TestWorkdayWorktimeDay(t *testing.T) {
workTime := testWorkDay.TimeWorkReal(testUser) testCases := []struct {
if workTime != time.Hour*8 { testName string
t.Errorf("Calc Worktime Default not working, time should be 8h, but was %s", helper.FormatDuration(workTime))
}
}
func TestCalcWorkPauseDiff(t *testing.T) {
type testCase struct {
Name string
bookings []models.Booking bookings []models.Booking
expectedWorkTime time.Duration expectedTime time.Duration
expectedPauseTime time.Duration }{
expectedOvertime time.Duration {
} testName: "Bookings6hrs",
testCases := []testCase{testCase{
Name: "6hrs no pause",
bookings: testBookings6hrs, bookings: testBookings6hrs,
expectedWorkTime: 6 * time.Hour, expectedTime: time.Hour * 6,
expectedPauseTime: 0,
expectedOvertime: -2 * time.Hour,
}, },
testCase{ {
Name: "8hrs - 30min pause", testName: "Bookings8hrs",
bookings: testBookings8hrs, bookings: testBookings8hrs,
expectedWorkTime: 7*time.Hour + 30*time.Minute, expectedTime: time.Hour*7 + time.Minute*30,
expectedPauseTime: 30 * time.Minute,
expectedOvertime: -30 * time.Minute,
}, },
testCase{ {
Name: "10hrs - 45min pause", testName: "Bookings10hrs",
bookings: testBookings10hrs, bookings: testBookings10hrs,
expectedWorkTime: 9*time.Hour + 15*time.Minute, expectedTime: time.Hour*9 + time.Minute*15,
expectedPauseTime: 45 * time.Minute, },
expectedOvertime: 1*time.Hour + 15*time.Minute, }
}}
for _, test := range testCases { for _, tc := range testCases {
t.Run(test.Name, func(t *testing.T) { t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) {
testWorkDay.Bookings = test.bookings var testCase = testWorkDay
testWorkDay.TimeWorkReal(testUser) testCase.Bookings = tc.bookings
testWorkDay.TimePauseReal(testUser) workTime := testCase.GetWorktime(testUser, models.WorktimeBaseDay, false)
testWorkDay.TimeOvertimeReal(testUser) if workTime != tc.expectedTime {
workTime, pauseTime, overTime := testWorkDay.GetAllWorkTimesReal(testUser) t.Errorf("GetWorktimeReal not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true))
if workTime != test.expectedWorkTime { }
t.Errorf("Calculated wrong workTime: should be %s, but was %s", helper.FormatDuration(test.expectedWorkTime), helper.FormatDuration(workTime)) })
} }
if pauseTime != test.expectedPauseTime { }
t.Errorf("Calculated wrong pauseTime: should be %s, but was %s", helper.FormatDuration(test.expectedPauseTime), helper.FormatDuration(pauseTime))
} func TestWorkdayWorktimeWeek(t *testing.T) {
if overTime != test.expectedOvertime { testCases := []struct {
t.Errorf("Calculated wrong overtime: should be %s, but was %s", helper.FormatDuration(test.expectedOvertime), helper.FormatDuration(overTime)) testName string
bookings []models.Booking
expectedTime time.Duration
}{
{
testName: "Bookings6hrs",
bookings: testBookings6hrs,
expectedTime: time.Hour * 6,
},
{
testName: "Bookings8hrs",
bookings: testBookings8hrs,
expectedTime: time.Hour*7 + time.Minute*30,
},
{
testName: "Bookings10hrs",
bookings: testBookings10hrs,
expectedTime: time.Hour*9 + time.Minute*15,
},
}
for _, tc := range testCases {
t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) {
var testCase = testWorkDay
testCase.Bookings = tc.bookings
workTime := testCase.GetWorktime(testUser, models.WorktimeBaseWeek, false)
if workTime != tc.expectedTime {
t.Errorf("GetWorktimeReal not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true))
}
})
}
}
func TestWorkdayPausetimeDay(t *testing.T) {
testCases := []struct {
testName string
bookings []models.Booking
expectedTime time.Duration
}{
{
testName: "Bookings6hrs",
bookings: testBookings6hrs,
expectedTime: 0,
},
{
testName: "Bookings8hrs",
bookings: testBookings8hrs,
expectedTime: time.Minute * 30,
},
{
testName: "Bookings10hrs",
bookings: testBookings10hrs,
expectedTime: time.Minute * 45,
},
}
for _, tc := range testCases {
t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) {
var testCase = testWorkDay
testCase.Bookings = tc.bookings
workTime := testCase.GetPausetime(testUser, models.WorktimeBaseDay, false)
if workTime != tc.expectedTime {
t.Errorf("GetPausetimeReal not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true))
}
})
}
}
func TestWorkdayPausetimeWeek(t *testing.T) {
testCases := []struct {
testName string
bookings []models.Booking
expectedTime time.Duration
}{
{
testName: "Bookings6hrs",
bookings: testBookings6hrs,
expectedTime: 0,
},
{
testName: "Bookings8hrs",
bookings: testBookings8hrs,
expectedTime: time.Minute * 30,
},
{
testName: "Bookings10hrs",
bookings: testBookings10hrs,
expectedTime: time.Minute * 45,
},
}
for _, tc := range testCases {
t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) {
var testCase = testWorkDay
testCase.Bookings = tc.bookings
workTime := testCase.GetPausetime(testUser, models.WorktimeBaseWeek, false)
if workTime != tc.expectedTime {
t.Errorf("GetPausetimeReal not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true))
} }
}) })
} }

View File

@@ -4,7 +4,10 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"log" "log"
"log/slog"
"time" "time"
"github.com/lib/pq"
) )
// Workweeks are // Workweeks are
@@ -17,10 +20,9 @@ type WorkWeek struct {
User User User User
WeekStart time.Time WeekStart time.Time
Worktime time.Duration Worktime time.Duration
WorktimeVirtual time.Duration
Overtime time.Duration Overtime time.Duration
Status WeekStatus Status WeekStatus
overtimeDiff time.Duration
worktimeDiff time.Duration
} }
type WeekStatus int8 type WeekStatus int8
@@ -45,12 +47,19 @@ func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek {
} }
func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Duration) { func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Duration) {
slog.Debug("Populating Workweek for user", "user", w.User)
slog.Debug("Got Days with overtime and worktime", slog.String("worktime", worktime.String()), slog.String("overtime", overtime.String()))
w.Days = GetDays(w.User, w.WeekStart, w.WeekStart.Add(6*24*time.Hour), false) w.Days = GetDays(w.User, w.WeekStart, w.WeekStart.Add(6*24*time.Hour), false)
for _, day := range w.Days { for _, day := range w.Days {
w.Worktime += day.TimeWorkVirtual(w.User) w.Worktime += day.GetWorktime(w.User, WorktimeBaseDay, false)
w.WorktimeVirtual += day.GetWorktime(w.User, WorktimeBaseDay, true)
} }
w.Overtime = w.Worktime - w.User.ArbeitszeitProWoche() slog.Debug("Got worktime for user", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorktimeVirtual.String())
w.Overtime = w.WorktimeVirtual - w.User.ArbeitszeitProWoche()
slog.Debug("Calculated overtime", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorktimeVirtual.String())
w.Worktime = w.Worktime.Round(time.Minute) w.Worktime = w.Worktime.Round(time.Minute)
w.Overtime = w.Overtime.Round(time.Minute) w.Overtime = w.Overtime.Round(time.Minute)
@@ -61,8 +70,6 @@ func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Durati
if overtime != w.Overtime || worktime != w.Worktime { if overtime != w.Overtime || worktime != w.Worktime {
w.Status = WeekStatusDifferences w.Status = WeekStatusDifferences
w.overtimeDiff = overtime
w.worktimeDiff = worktime
} }
} }
@@ -127,12 +134,13 @@ func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var week WorkWeek = WorkWeek{User: user} var week WorkWeek = WorkWeek{User: user}
if err := rows.Scan(&week.Id, &week.WeekStart, &week.Worktime, &week.Overtime); err != nil { var workTime, overTime time.Duration
if err := rows.Scan(&week.Id, &week.WeekStart, &workTime, &overTime); err != nil {
log.Println("Error scanning row!", err) log.Println("Error scanning row!", err)
return weeks return weeks
} }
week.PopulateWithDays(week.Worktime, week.Overtime) week.PopulateWithDays(workTime, overTime)
weeks = append(weeks, week) weeks = append(weeks, week)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
@@ -144,30 +152,70 @@ func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek {
var ErrRunningWeek = errors.New("Week is in running week") var ErrRunningWeek = errors.New("Week is in running week")
func (w *WorkWeek) GetBookingIds() (anwesenheitsIds, abwesenheitsIds []int64, err error) {
qStr, err := DB.Prepare(`
SELECT
(SELECT array_agg(counter_id ORDER BY counter_id)
FROM anwesenheit
WHERE card_uid = $1
AND timestamp::DATE >= $2
AND timestamp::DATE < $3) AS anwesenheit,
(SELECT array_agg(counter_id ORDER BY counter_id)
FROM abwesenheit
WHERE card_uid = $1
AND datum_from < $3
AND datum_to >= $2) AS abwesenheit;
`)
if err != nil {
return nil, nil, err
}
defer qStr.Close()
slog.Debug("Inserting parameters into qStr:", "user card_uid", w.User.CardUID, "week_start", w.WeekStart, "week_end", w.WeekStart.AddDate(0, 0, 5))
err = qStr.QueryRow(w.User.CardUID, w.WeekStart, w.WeekStart.AddDate(0, 0, 5)).Scan(pq.Array(&anwesenheitsIds), pq.Array(&abwesenheitsIds))
if err != nil {
return anwesenheitsIds, abwesenheitsIds, err
}
return anwesenheitsIds, abwesenheitsIds, nil
}
// creates a new entry in the woche_report table with the given workweek // creates a new entry in the woche_report table with the given workweek
func (w *WorkWeek) SendWeek() error { func (w *WorkWeek) SendWeek() error {
var qStr *sql.Stmt var qStr *sql.Stmt
var err error var err error
slog.Info("Sending workWeek to team head", "week", w.WeekStart.String())
anwBookings, awBookings, err := w.GetBookingIds()
if err != nil {
slog.Warn("Error querying bookings from work week", slog.Any("error", err))
return err
}
slog.Debug("Recieved Booking Ids", "anwesenheiten", anwBookings)
if time.Since(w.WeekStart) < 5*24*time.Hour { if time.Since(w.WeekStart) < 5*24*time.Hour {
log.Println("Cannot send week, because it's the running week!") slog.Warn("Cannot send week, because it's the running week!")
return ErrRunningWeek return ErrRunningWeek
} }
if w.CheckStatus() != WeekStatusNone { 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) WHERE personal_nummer = $1 AND woche_start = $2;`) 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 { if err != nil {
log.Println("Error preparing SQL statement", err) slog.Warn("Error preparing SQL statement", "error", err)
return err return err
} }
} else { } else {
qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden) VALUES ($1, $2, make_interval(secs => $3::numeric / 1000000000), make_interval(secs => $4::numeric / 1000000000));`) qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden, anwesenheiten, abwesenheiten) VALUES ($1, $2, make_interval(secs => $3::numeric / 1000000000), make_interval(secs => $4::numeric / 1000000000), $5, $6);`)
if err != nil { if err != nil {
log.Println("Error preparing SQL statement", err) slog.Warn("Error preparing SQL statement", "error", err)
return err return err
} }
} }
_, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, int64(w.Worktime), int64(w.Overtime))
_, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, int64(w.Worktime), int64(w.Overtime), pq.Array(anwBookings), pq.Array(awBookings))
if err != nil { if err != nil {
log.Println("Error executing query!", err) log.Println("Error executing query!", err)
return err return err

View File

@@ -8,7 +8,7 @@ import (
func SetupWorkWeekFixture(t *testing.T) models.WorkWeek { func SetupWorkWeekFixture(t *testing.T) models.WorkWeek {
t.Helper() t.Helper()
monday, err := time.Parse("2006-01-02", "2025-01-10") monday, err := time.Parse(time.DateOnly, "2025-01-10")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -16,7 +16,7 @@ func SetupWorkWeekFixture(t *testing.T) models.WorkWeek {
} }
func TestNewWorkWeekNoPopulate(t *testing.T) { func TestNewWorkWeekNoPopulate(t *testing.T) {
monday, err := time.Parse("2006-01-02", "2025-01-10") monday, err := time.Parse(time.DateOnly, "2025-01-10")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -1,6 +1,6 @@
sonar.projectKey=Arbeitszeitmessung sonar.projectKey=arbeitszeitmessung
sonar.sources=. sonar.sources=.
sonar.exclusions=**/*_test.go sonar.exclusions=**/*_test.go, **/*_templ.go
sonar.tests=. sonar.tests=.
sonar.test.inclusions=**/*_test.go sonar.test.inclusions=**/*_test.go

View File

@@ -41,7 +41,7 @@
@layer components { @layer components {
.grid-main { .grid-main {
display: grid; display: grid;
grid-template-columns: 4fr 6fr 1fr; grid-template-columns: 4fr 3fr 3fr 1fr;
align-items: stretch; align-items: stretch;
} }
@@ -78,8 +78,14 @@
border-style: var(--tw-border-style); border-style: var(--tw-border-style);
border-width: 1px; border-width: 1px;
border-color: var(--color-neutral-800); border-color: var(--color-neutral-800);
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; transition-property:
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); 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)); transition-duration: var(--tw-duration, var(--default-transition-duration));
} }
@@ -135,6 +141,11 @@
outline: none; outline: none;
} }
div.edit {
border-width: 1px;
background-color: var(--color-neutral-300);
}
@media (width >=48rem) { @media (width >=48rem) {
.grid-main { .grid-main {
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);

View File

@@ -12,7 +12,10 @@
--color-red-700: oklch(50.5% 0.213 27.518); --color-red-700: oklch(50.5% 0.213 27.518);
--color-orange-500: oklch(70.5% 0.213 47.604); --color-orange-500: oklch(70.5% 0.213 47.604);
--color-purple-600: oklch(55.8% 0.288 302.321); --color-purple-600: oklch(55.8% 0.288 302.321);
--color-slate-300: oklch(86.9% 0.022 252.894);
--color-slate-600: oklch(44.6% 0.043 257.281);
--color-slate-700: oklch(37.2% 0.044 257.287); --color-slate-700: oklch(37.2% 0.044 257.287);
--color-slate-800: oklch(27.9% 0.041 260.031);
--color-neutral-100: oklch(97% 0 0); --color-neutral-100: oklch(97% 0 0);
--color-neutral-200: oklch(92.2% 0 0); --color-neutral-200: oklch(92.2% 0 0);
--color-neutral-300: oklch(87% 0 0); --color-neutral-300: oklch(87% 0 0);
@@ -197,6 +200,15 @@
.relative { .relative {
position: relative; position: relative;
} }
.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-2\.5 {
top: calc(var(--spacing) * 2.5); top: calc(var(--spacing) * 2.5);
} }
@@ -206,9 +218,18 @@
.right-1 { .right-1 {
right: calc(var(--spacing) * 1); right: calc(var(--spacing) * 1);
} }
.right-2 {
right: calc(var(--spacing) * 2);
}
.right-2\.5 { .right-2\.5 {
right: calc(var(--spacing) * 2.5); right: calc(var(--spacing) * 2.5);
} }
.left-1 {
left: calc(var(--spacing) * 1);
}
.left-1\/2 {
left: calc(1/2 * 100%);
}
.col-span-2 { .col-span-2 {
grid-column: span 2 / span 2; grid-column: span 2 / span 2;
} }
@@ -236,6 +257,22 @@
.ml-1 { .ml-1 {
margin-left: calc(var(--spacing) * 1); margin-left: calc(var(--spacing) * 1);
} }
.ml-2 {
margin-left: calc(var(--spacing) * 2);
}
.icon-\[material-symbols-light--cancel-outline\] {
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='m8.4 16.308l3.6-3.6l3.6 3.6l.708-.708l-3.6-3.6l3.6-3.6l-.708-.708l-3.6 3.6l-3.6-3.6l-.708.708l3.6 3.6l-3.6 3.6zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");
}
.icon-\[material-symbols-light--check-circle-outline\] { .icon-\[material-symbols-light--check-circle-outline\] {
display: inline-block; display: inline-block;
width: 1.25em; width: 1.25em;
@@ -301,19 +338,6 @@
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='M9.808 14.616h1V9.385h-1zm3.384 0h1V9.385h-1zM12.003 21q-1.866 0-3.51-.705q-1.643-.706-2.859-1.915t-1.925-2.843T3 12.039q0-.905.167-1.778t.497-1.713l.78.78q-.219.65-.331 1.32T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.675 0-1.332.112t-1.3.332l-.776-.775q.789-.315 1.606-.492T11.885 3q1.887 0 3.546.701t2.894 1.926t1.955 2.866t.72 3.505t-.708 3.509t-1.924 2.859t-2.856 1.925t-3.509.709M5.923 6.808q-.356 0-.62-.265q-.264-.264-.264-.62t.264-.62t.62-.264t.62.264t.265.62t-.265.62t-.62.265M12 12'/%3E%3C/svg%3E"); --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9.808 14.616h1V9.385h-1zm3.384 0h1V9.385h-1zM12.003 21q-1.866 0-3.51-.705q-1.643-.706-2.859-1.915t-1.925-2.843T3 12.039q0-.905.167-1.778t.497-1.713l.78.78q-.219.65-.331 1.32T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.675 0-1.332.112t-1.3.332l-.776-.775q.789-.315 1.606-.492T11.885 3q1.887 0 3.546.701t2.894 1.926t1.955 2.866t.72 3.505t-.708 3.509t-1.924 2.859t-2.856 1.925t-3.509.709M5.923 6.808q-.356 0-.62-.265q-.264-.264-.264-.62t.264-.62t.62-.264t.62.264t.265.62t-.265.62t-.62.265M12 12'/%3E%3C/svg%3E");
} }
.icon-\[material-symbols-light--nest-clock-farsight-analog-outline\] {
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='M14.935 16.223L11.5 12.789V7.923h1v4.464l3.123 3.123zM11.5 6V4h1v2zm6.5 6.5v-1h2v1zM11.5 20v-2h1v2zM4 12.5v-1h2v1zm8.003 8.5q-1.867 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");
}
.icon-\[material-symbols-light--schedule-outline\] { .icon-\[material-symbols-light--schedule-outline\] {
display: inline-block; display: inline-block;
width: 1.25em; width: 1.25em;
@@ -342,6 +366,9 @@
.inline { .inline {
display: inline; display: inline;
} }
.inline-flex {
display: inline-flex;
}
.table { .table {
display: table; display: table;
} }
@@ -353,9 +380,19 @@
width: calc(var(--spacing) * 4); width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4); height: calc(var(--spacing) * 4);
} }
.size-5 {
width: calc(var(--spacing) * 5);
height: calc(var(--spacing) * 5);
}
.h-2 { .h-2 {
height: calc(var(--spacing) * 2); height: calc(var(--spacing) * 2);
} }
.h-3 {
height: calc(var(--spacing) * 3);
}
.h-3\.5 {
height: calc(var(--spacing) * 3.5);
}
.h-4 { .h-4 {
height: calc(var(--spacing) * 4); height: calc(var(--spacing) * 4);
} }
@@ -374,12 +411,21 @@
.w-2 { .w-2 {
width: calc(var(--spacing) * 2); width: calc(var(--spacing) * 2);
} }
.w-3 {
width: calc(var(--spacing) * 3);
}
.w-3\.5 {
width: calc(var(--spacing) * 3.5);
}
.w-4 { .w-4 {
width: calc(var(--spacing) * 4); width: calc(var(--spacing) * 4);
} }
.w-5 { .w-5 {
width: calc(var(--spacing) * 5); width: calc(var(--spacing) * 5);
} }
.w-9 {
width: calc(var(--spacing) * 9);
}
.w-9\/10 { .w-9\/10 {
width: calc(9/10 * 100%); width: calc(9/10 * 100%);
} }
@@ -392,6 +438,9 @@
.w-full { .w-full {
width: 100%; width: 100%;
} }
.flex-shrink {
flex-shrink: 1;
}
.flex-shrink-0 { .flex-shrink-0 {
flex-shrink: 0; flex-shrink: 0;
} }
@@ -407,9 +456,34 @@
.basis-\[content\] { .basis-\[content\] {
flex-basis: content; flex-basis: content;
} }
.border-collapse {
border-collapse: collapse;
}
.-translate-x-1 {
--tw-translate-x: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-x-1\/2 {
--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);
}
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.resize {
resize: both;
}
.scroll-m-2 { .scroll-m-2 {
scroll-margin: calc(var(--spacing) * 2); scroll-margin: calc(var(--spacing) * 2);
} }
@@ -543,6 +617,15 @@
.border-neutral-600 { .border-neutral-600 {
border-color: var(--color-neutral-600); border-color: var(--color-neutral-600);
} }
.border-slate-300 {
border-color: var(--color-slate-300);
}
.border-slate-700 {
border-color: var(--color-slate-700);
}
.border-slate-800 {
border-color: var(--color-slate-800);
}
.bg-accent { .bg-accent {
background-color: var(--color-accent); background-color: var(--color-accent);
} }
@@ -564,6 +647,9 @@
.bg-red-600 { .bg-red-600 {
background-color: var(--color-red-600); background-color: var(--color-red-600);
} }
.mask-repeat {
mask-repeat: repeat;
}
.p-1 { .p-1 {
padding: calc(var(--spacing) * 1); padding: calc(var(--spacing) * 1);
} }
@@ -628,12 +714,28 @@
.text-red-600 { .text-red-600 {
color: var(--color-red-600); color: var(--color-red-600);
} }
.text-slate-600 {
color: var(--color-slate-600);
}
.text-slate-700 { .text-slate-700 {
color: var(--color-slate-700); color: var(--color-slate-700);
} }
.text-white {
color: var(--color-white);
}
.uppercase { .uppercase {
text-transform: 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 {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
} }
@@ -642,6 +744,11 @@
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration)); transition-duration: var(--tw-duration, var(--default-transition-duration));
} }
.transition-all {
transition-property: all;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.transition-colors { .transition-colors {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
@@ -651,6 +758,10 @@
--tw-duration: 300ms; --tw-duration: 300ms;
transition-duration: 300ms; transition-duration: 300ms;
} }
.select-none {
-webkit-user-select: none;
user-select: none;
}
.\*\:text-center { .\*\:text-center {
:is(& > *) { :is(& > *) {
text-align: center; text-align: center;
@@ -682,11 +793,6 @@
margin-left: calc(var(--spacing) * 2); margin-left: calc(var(--spacing) * 2);
} }
} }
.group-\[\.edit\]\:block {
&:is(:where(.group):is(.edit) *) {
display: block;
}
}
.group-\[\.edit\]\:flex { .group-\[\.edit\]\:flex {
&:is(:where(.group):is(.edit) *) { &:is(:where(.group):is(.edit) *) {
display: flex; display: flex;
@@ -702,11 +808,36 @@
display: inline; display: inline;
} }
} }
.group-\[\.edit\]\/button\:block {
&:is(:where(.group\/button):is(.edit) *) {
display: block;
}
}
.group-\[\.edit\]\/button\:hidden {
&:is(:where(.group\/button):is(.edit) *) {
display: none;
}
}
.peer-checked\:opacity-100 {
&:is(:where(.peer):checked ~ *) {
opacity: 100%;
}
}
.placeholder\:text-neutral-400 { .placeholder\:text-neutral-400 {
&::placeholder { &::placeholder {
color: var(--color-neutral-400); color: var(--color-neutral-400);
} }
} }
.checked\:border-slate-800 {
&:checked {
border-color: var(--color-slate-800);
}
}
.checked\:bg-slate-800 {
&:checked {
background-color: var(--color-slate-800);
}
}
.hover\:border-neutral-500 { .hover\:border-neutral-500 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@@ -789,12 +920,6 @@
} }
} }
} }
.max-md\:border-b-1 {
@media (width < 48rem) {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px;
}
}
.max-md\:bg-neutral-300 { .max-md\:bg-neutral-300 {
@media (width < 48rem) { @media (width < 48rem) {
background-color: var(--color-neutral-300); background-color: var(--color-neutral-300);
@@ -810,11 +935,6 @@
grid-column: span 3 / span 3; grid-column: span 3 / span 3;
} }
} }
.md\:col-span-4 {
@media (width >= 48rem) {
grid-column: span 4 / span 4;
}
}
.md\:mx-\[10\%\] { .md\:mx-\[10\%\] {
@media (width >= 48rem) { @media (width >= 48rem) {
margin-inline: 10%; margin-inline: 10%;
@@ -840,6 +960,11 @@
width: calc(1/2 * 100%); width: calc(1/2 * 100%);
} }
} }
.md\:flex-row {
@media (width >= 48rem) {
flex-direction: row;
}
}
.md\:px-4 { .md\:px-4 {
@media (width >= 48rem) { @media (width >= 48rem) {
padding-inline: calc(var(--spacing) * 4); padding-inline: calc(var(--spacing) * 4);
@@ -850,8 +975,8 @@
color: transparent; color: transparent;
} }
} }
.group-\[\.edit\]\:md\:block { .group-\[\.edit\]\/button\:md\:block {
&:is(:where(.group):is(.edit) *) { &:is(:where(.group\/button):is(.edit) *) {
@media (width >= 48rem) { @media (width >= 48rem) {
display: block; display: block;
} }
@@ -909,7 +1034,7 @@
@layer components { @layer components {
.grid-main { .grid-main {
display: grid; display: grid;
grid-template-columns: 4fr 6fr 1fr; grid-template-columns: 4fr 3fr 3fr 1fr;
align-items: stretch; align-items: stretch;
} }
.grid-sub { .grid-sub {
@@ -985,6 +1110,10 @@
.edit-box input:focus { .edit-box input:focus {
outline: none; outline: none;
} }
div.edit {
border-width: 1px;
background-color: var(--color-neutral-300);
}
@media (width >=48rem) { @media (width >=48rem) {
.grid-main { .grid-main {
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
@@ -998,6 +1127,41 @@
} }
} }
} }
@property --tw-translate-x {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-y {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-z {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
}
@property --tw-divide-x-reverse { @property --tw-divide-x-reverse {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@@ -1017,6 +1181,11 @@
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
} }
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur { @property --tw-blur {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@@ -1077,10 +1246,19 @@
@layer properties { @layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop { *, ::before, ::after, ::backdrop {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-translate-z: 0;
--tw-rotate-x: initial;
--tw-rotate-y: initial;
--tw-rotate-z: initial;
--tw-skew-x: initial;
--tw-skew-y: initial;
--tw-divide-x-reverse: 0; --tw-divide-x-reverse: 0;
--tw-border-style: solid; --tw-border-style: solid;
--tw-divide-y-reverse: 0; --tw-divide-y-reverse: 0;
--tw-font-weight: initial; --tw-font-weight: initial;
--tw-outline-style: solid;
--tw-blur: initial; --tw-blur: initial;
--tw-brightness: initial; --tw-brightness: initial;
--tw-contrast: initial; --tw-contrast: initial;

BIN
Backend/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,59 +1,87 @@
function editDay(element, event, formId) { function clearEditState() {
var form = element.closest(".grid-sub").querySelector(".all-booking-component > form"); for (let e of document.querySelectorAll(".edit")) {
form.classList.toggle("edit"); e.classList.remove("edit");
element.classList.toggle("edit"); }
if (element.classList.contains("edit")) { toggleAbsenceEdit(false);
}
function clearButtonState() {
for (let b of document.querySelectorAll(".change-button-component")) {
b.type = "button";
}
}
function editWorkday(element, event, id, isWorkDay) {
event.preventDefault(); event.preventDefault();
form.querySelectorAll("input, select").forEach((input) => { let form = document.getElementById(id);
if (form == null) {
form = element.closest(".grid-sub").querySelector(".all-booking-component > form");
}
clearEditState();
element.closest(".grid-sub").classList.add("edit");
toggleAbsenceEdit(!isWorkDay);
if (isWorkDay) {
element.classList.add("edit");
if (element.type == "button") {
for (let input of form.querySelectorAll("input, select")) {
input.disabled = false; input.disabled = false;
}); }
clearButtonState();
element.type = "submit";
} else { } else {
form.submit(); form.submit();
} }
} else {
const absenceForm = document.getElementById("absence_form");
if (id == 0) {
absenceForm.querySelector("[name=date_from]").value = form.id.replace("time-", "");
absenceForm.querySelector("[name=date_to]").value = form.id.replace("time-", "");
} else {
syncFields(form, absenceForm, ["date_from", "date_to", "aw_type", "aw_id"]);
}
}
}
function toggleAbsenceEdit(state) {
const form = document.getElementById("absence_form");
if (state) {
form.classList.remove("hidden");
form.scrollIntoView({
behavior: "smooth",
block: "start",
inline: "nearest",
});
} else {
form.classList.add("hidden");
}
} }
function syncFields(from, to, fieldsToSync) { function syncFields(from, to, fieldsToSync) {
fieldsToSync.forEach((name) => { for (let field of fieldsToSync) {
const src = from.querySelector(`[name=${name}]`); const src = from.querySelector(`[name=${field}]`);
const target = to.querySelector(`[name=${name}]`); const target = to.querySelector(`[name=${field}]`);
if (!src || !target) return; if (!src || !target) return;
target.value = src.value; target.value = src.value;
});
}
function editAbsence(element, event, absenceId) {
event.preventDefault();
var form = document.getElementById("absence_form");
if (absenceId != 0) {
var dataForm = document.getElementById(absenceId);
syncFields(dataForm, form, ["date_from", "date_to", "aw_type", "aw_id"]);
} else {
var dataForm = element.closest(".grid-sub").querySelector(".all-booking-component > form");
form.querySelector("[name=date_from]").value = dataForm.id.replace("time-", "");
form.querySelector("[name=date_to]").value = dataForm.id.replace("time-", "");
}
form.classList.remove("hidden");
form.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
}
function editAbwesenheit(element, event) {
var newBookingComponent = element.closest(".grid-sub").querySelector(".new-booking-component");
if (element.value == 0) {
newBookingComponent.style.display = "";
} else {
newBookingComponent.style.display = "none";
} }
} }
function navigateWeek(element, event, direction) { function navigateWeek(element, event, direction) {
var dateInput = element.closest("form").querySelector("input[type=date]"); const dateInput = element.closest("form").querySelector("input[type=date]");
var date = dateInput.valueAsDate; const date = dateInput.valueAsDate;
date.setDate(date.getDate() + 7 * direction); date.setDate(date.getDate() + 7 * direction);
date.setHours(10); date.setHours(10);
dateInput.valueAsDate = date; dateInput.valueAsDate = date;
} }
function logoutUser() { function logoutUser() {
fetch("/user/logout", {}).then(() => window.location.reload()); fetch("/user/logout", {}).then(() => globalThis.location.reload());
}
function checkAll(pattern, state) {
for (let input of document.querySelectorAll(`input[id^=${pattern}]`)) {
input.checked = state;
}
} }

92
Backend/template.typ Normal file
View File

@@ -0,0 +1,92 @@
#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

@@ -4,8 +4,9 @@ templ headerComponent() {
<div class="flex flex-row justify-between md:mx-[10%] py-2 items-center"> <div class="flex flex-row justify-between md:mx-[10%] py-2 items-center">
<a href="/time">Zeitverwaltung</a> <a href="/time">Zeitverwaltung</a>
<a href="/team">Abrechnung</a> <a href="/team">Abrechnung</a>
<a href="/pdf">PDF</a>
if true { if true {
<a href="/team/presence">Anwesenheit</a> <a href="/presence">Anwesenheit</a>
} }
<a href="/user/settings">Einstellungen</a> <a href="/user/settings">Einstellungen</a>
@LogoutButton() @LogoutButton()

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.924 // templ: version: v0.3.943
package templates package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -29,12 +29,12 @@ func headerComponent() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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> ") 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> <a href=\"/pdf\">PDF</a> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if true { if true {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"/team/presence\">Anwesenheit</a> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"/presence\">Anwesenheit</a> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -28,7 +28,10 @@ templ LoginPage(success bool, errorMsg string) {
</div> </div>
} }
templ UserPage(status int) { templ SettingsPage(status int) {
{{
user := ctx.Value("user").(models.User)
}}
@Base() @Base()
@headerComponent() @headerComponent()
<div class="grid-main divide-y-1"> <div class="grid-main divide-y-1">
@@ -51,6 +54,14 @@ templ UserPage(status int) {
<button name="action" value="change-pass" type="submit" class="btn">Ändern</button> <button name="action" value="change-pass" type="submit" class="btn">Ändern</button>
</div> </div>
</form> </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">{ user.Vorname } { user.Name }</span></p>
<p>Personalnummer: <span class="text-neutral-500">{ user.PersonalNummer }</span></p>
</div>
<div></div>
</div>
<div class="grid-sub responsive lg:divide-x-1"> <div class="grid-sub responsive lg:divide-x-1">
<h1 class="grid-cell font-bold uppercase text-xl text-center">Nutzer abmelden</h1> <h1 class="grid-cell font-bold uppercase text-xl text-center">Nutzer abmelden</h1>
<div class="grid-cell col-span-3"> <div class="grid-cell col-span-3">
@@ -77,68 +88,13 @@ templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) {
<div class="grid-main divide-y-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-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"> <div class="grid-cell col-span-full bg-neutral-300 lg:border-0">
<h2 class="text-2xl uppercase font-bold">Eigene Abrechnung</h2> <h2 class="text-xl uppercase font-bold">Eigene Abrechnung</h2>
</div> </div>
</div> </div>
@workWeekComponent(userWeek, false) @workWeekComponent(userWeek, false)
// <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-2xl uppercase font-bold">Eigene Abrechnung</h2>
// </div>
// <div class="grid-cell flex flex-col max-md:border-b-1 max-md:bg-neutral-300 gap-2 ">
// <div class="lg:hidden">
// @weekPicker(userWeek.WeekStart)
// </div>
// <h2 class="uppercase font-bold">{ fmt.Sprintf("%s %s", userWeek.User.Vorname, userWeek.User.Name) }</h2>
// <div class="grid grid-cols-5 gap-2 lg:grid-cols-1">
// <div class="col-span-2">
// <span class="flex flex-row gap-2 items-center">
// @statusCheckMark(userWeek.CheckStatus(), models.WeekStatusSent)
// Gesendet
// </span>
// <span class="flex flex-row gap-2 items-center">
// @statusCheckMark(userWeek.CheckStatus(), models.WeekStatusAccepted)
// Akzeptiert
// </span>
// </div>
// <div class="flex flex-row gap-2 col-span-3">
// @timeGaugeComponent(int8(progress), false)
// <div>
// <p>Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDuration(userWeek.Worktime)) }</p>
// <p>Überstunden: { fmt.Sprintf("%s", helper.FormatDurationFill(userWeek.Overtime, true)) }</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">
// for _, day := range userWeek.Days {
// @defaultWeekDayComponent(userWeek.User, day)
// }
// </div>
// <div class="grid-cell flex flex-col gap-2 justify-between">
// <div class="max-md:hidden">
// @weekPicker(userWeek.WeekStart)
// </div>
// <form method="post" class="flex flex-col gap-2">
// <input type="hidden" name="method" value="send"/>
// <input type="hidden" name="user" value={ strconv.Itoa(userWeek.User.PersonalNummer) }/>
// <input type="hidden" name="week" value={ userWeek.WeekStart.Format(time.DateOnly) }/>
// switch userWeek.CheckStatus() {
// case models.WeekStatusNone:
// <p class="text-sm">an Vorgesetzten senden</p>
// case models.WeekStatusSent:
// <p class="text-sm">an Vorgesetzten gesendet</p>
// case models.WeekStatusAccepted:
// <p class="text-sm">vom Vorgesetzten bestätigt</p>
// }
// <button disabled?={ userWeek.Status < models.WeekStatusSent } type="submit" class="btn">Korrigieren</button>
// <button disabled?={ time.Since(userWeek.WeekStart) < 24*7*time.Hour || userWeek.Status >= models.WeekStatusSent || userWeek.RequiresAction() } type="submit" class="btn">Senden</button>
// </form>
// </div>
// </div>
if len(weeks) > 0 { if len(weeks) > 0 {
<div class="grid-cell col-span-full bg-neutral-300"> <div class="grid-cell col-span-full bg-neutral-300">
<h2 class="text-2xl uppercase font-bold">Abrechnung Mitarbeiter</h2> <h2 class="text-xl uppercase font-bold">Abrechnung Mitarbeiter</h2>
</div> </div>
} }
for _, week := range weeks { for _, week := range weeks {
@@ -147,29 +103,6 @@ templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) {
</div> </div>
} }
templ TeamPresencePage(teamPresence map[bool][]models.User) {
@Base()
@headerComponent()
<div class="grid-main divide-y-1">
<div class="grid-sub divide-x-1">
<h2 class="grid-cell font-bold uppercase">Anwesend</h2>
<div class="flex flex-col col-span-2 md:col-span-4">
for _, user := range teamPresence[true] {
@userPresenceComponent(user, true)
}
</div>
</div>
<div class="grid-sub divide-x-1">
<h2 class="grid-cell font-bold uppercase">Nicht Anwesend</h2>
<div class="flex flex-col col-span-2 md:col-span-4">
for _, user := range teamPresence[false] {
@userPresenceComponent(user, false)
}
</div>
</div>
</div>
}
templ LogoutButton() { templ LogoutButton() {
<button onclick="logoutUser()" type="button" class="cursor-pointer">Abmelden</button> <button onclick="logoutUser()" type="button" class="cursor-pointer">Abmelden</button>
} }

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.924 // templ: version: v0.3.943
package templates package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -95,7 +95,7 @@ func LoginPage(success bool, errorMsg string) templ.Component {
}) })
} }
func UserPage(status int) templ.Component { func SettingsPage(status int) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -116,6 +116,8 @@ func UserPage(status int) templ.Component {
templ_7745c5c3_Var4 = templ.NopComponent templ_7745c5c3_Var4 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
user := ctx.Value("user").(models.User)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
@@ -145,7 +147,46 @@ func UserPage(status int) templ.Component {
return templ_7745c5c3_Err 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\">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>") 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: 60, 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: 60, 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: 61, 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></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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -169,18 +210,18 @@ func statusCheckMark(status models.WeekStatus, target models.WeekStatus) templ.C
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx) templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil { if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var5 = templ.NopComponent templ_7745c5c3_Var8 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
if status >= target { if status >= target {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"icon-[material-symbols-light--check-circle-outline]\"></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"icon-[material-symbols-light--check-circle-outline]\"></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"icon-[material-symbols-light--circle-outline]\"></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"icon-[material-symbols-light--circle-outline]\"></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -205,9 +246,9 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx) templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil { if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var6 = templ.NopComponent templ_7745c5c3_Var9 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
@@ -218,7 +259,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<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-2xl uppercase font-bold\">Eigene Abrechnung</h2></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -227,7 +268,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(weeks) > 0 { if len(weeks) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"grid-cell col-span-full bg-neutral-300\"><h2 class=\"text-2xl uppercase font-bold\">Abrechnung Mitarbeiter</h2></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -238,64 +279,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func TeamPresencePage(teamPresence map[bool][]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_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = 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, 16, "<div class=\"grid-main divide-y-1\"><div class=\"grid-sub divide-x-1\"><h2 class=\"grid-cell font-bold uppercase\">Anwesend</h2><div class=\"flex flex-col col-span-2 md:col-span-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, user := range teamPresence[true] {
templ_7745c5c3_Err = userPresenceComponent(user, true).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><div class=\"grid-sub divide-x-1\"><h2 class=\"grid-cell font-bold uppercase\">Nicht Anwesend</h2><div class=\"flex flex-col col-span-2 md:col-span-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, user := range teamPresence[false] {
templ_7745c5c3_Err = userPresenceComponent(user, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -319,9 +303,9 @@ func LogoutButton() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx) templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil { if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var8 = templ.NopComponent templ_7745c5c3_Var10 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<button onclick=\"logoutUser()\" type=\"button\" class=\"cursor-pointer\">Abmelden</button>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<button onclick=\"logoutUser()\" type=\"button\" class=\"cursor-pointer\">Abmelden</button>")

View File

@@ -3,69 +3,128 @@ package templates
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"fmt"
"time" "time"
) )
templ PDFReportEmploye(e models.User, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) { templ PDFForm(teamMembers []models.User) {
{{
_, kw := tsStart.ISOWeek()
noBorder := ""
}}
@Base() @Base()
<content class="p-8 relative flex flex-col gap-4 break-after-page"> @headerComponent()
<div> <form class="grid-main divide-y-1" action="pdf/generate" method="get">
<h1 class="text-2xl font-bold">Kim Mustermensch</h1> <div class="grid-cell col-span-full bg-neutral-300">
<p>Zeitraum: <span>{ tsStart.Format("02.01.2006") }</span> - <span>{ tsEnd.Format("02.01.2006") }</span></p> <h1 class="text-xl uppercase font-bold">PDF Abrechnung erstellen</h1>
<p>Arbeitszeit: <span></span></p>
<p>Überstunden: <span></span></p>
</div> </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"> <div class="grid-sub divide-x-1 responsive">
<p class="bg-neutral-300 border-neutral-600">{ kw }</p> <div class="grid-cell">Zeitraum wählen</div>
<p class="bg-neutral-300 border-neutral-600">Kommen</p> <div class="grid-cell col-span-3">
<p class="bg-neutral-300 border-neutral-600">Gehen</p> <label class="block mb-1 text-sm text-neutral-700">Abrechnungsmonat</label>
<p class="bg-neutral-300 border-neutral-600">Arbeitsart</p> <input name="start_date" type="date" value={ helper.GetFirstOfMonth(time.Now()).Format(time.DateOnly) } class="btn bg-neutral-100"/>
<p class="bg-neutral-300 border-neutral-600">Stunden</p> </div>
<p class="bg-neutral-300 border-neutral-600">Pause</p> <div></div>
<p class="bg-neutral-300 border-neutral-600 border-r-0">Überstunden</p> </div>
for index, day := range workDays { <div class="grid-sub divide-x-1 responsive">
{{ <div class="grid-cell">Mitarbeiter wählen</div>
if index == len(workDays)-1 { <div class="grid-cell col-span-3 flex flex-col gap-2">
noBorder = "border-b-0" <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>
<p class={ noBorder }>{ day.Date().Format("02.01.2006") }</p> </div>
<div class={ "grid grid-cols-subgrid col-span-3 " + noBorder }> for _, member := range teamMembers {
if day.IsWorkDay() { @CheckboxComponent(member.PersonalNummer, fmt.Sprintf("%s %s", member.Vorname, member.Name))
{{
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() {
<p class="col-span-full">Kurzarbeit</p>
}
} else {
{{
absentDay, _ := day.(*models.Absence)
}}
<p class="col-span-full">{ absentDay.AbwesenheitTyp.Name }</p>
} }
</div> </div>
{{ work, pause, overtime := day.GetAllWorkTimesVirtual(e) }} <div></div>
@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> </div>
</content> <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) { templ ColorDuration(d time.Duration, classes string) {
{{ {{
color := "" color := ""

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.924 // templ: version: v0.3.943
package templates package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -11,10 +11,11 @@ import templruntime "github.com/a-h/templ/runtime"
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"fmt"
"time" "time"
) )
func PDFReportEmploye(e models.User, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) templ.Component { func PDFForm(teamMembers []models.User) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -35,234 +36,72 @@ func PDFReportEmploye(e models.User, workDays []models.IWorkDay, tsStart time.Ti
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
_, kw := tsStart.ISOWeek()
noBorder := ""
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<content class=\"p-8 relative flex flex-col gap-4 break-after-page\"><div><h1 class=\"text-2xl font-bold\">Kim Mustermensch</h1><p>Zeitraum: <span>") 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\">PDF Abrechnung 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(tsStart.Format("02.01.2006")) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(helper.GetFirstOfMonth(time.Now()).Format(time.DateOnly))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 18, Col: 52} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span> - <span>") 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("true")))
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(tsEnd.Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 18, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span></p><p>Arbeitszeit: <span></span></p><p>Überstunden: <span></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\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<button class=\"btn\" type=\"button\" onclick=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var3 templ.ComponentScript = templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("true"))
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(kw) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3.Call)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 23, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</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>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">Alle</button> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for index, day := range workDays { templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("false")))
if index == len(workDays)-1 {
noBorder = "border-b-0"
}
var templ_7745c5c3_Var5 = []any{noBorder}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button class=\"btn\" type=\"button\" onclick=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var4 templ.ComponentScript = templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("false"))
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String()) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4.Call)
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_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">Keine</button></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string for _, member := range teamMembers {
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006")) 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.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 36, Col: 59}
}
_, 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, 7, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 = []any{"grid grid-cols-subgrid col-span-3 " + noBorder}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).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_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if day.IsWorkDay() {
workDay, _ := day.(*models.WorkDay)
for bookingI := 0; bookingI < len(workDay.Bookings); bookingI += 2 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI].Timestamp.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 43, Col: 64}
}
_, 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, "</p><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI+1].Timestamp.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 44, Col: 66}
}
_, 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, 12, "</p><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI].BookingType.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 45, Col: 55}
}
_, 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, 13, "</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ") 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
}
if workDay.IsKurzArbeit() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<p class=\"col-span-full\">Kurzarbeit</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
absentDay, _ := day.(*models.Absence)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<p class=\"col-span-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(absentDay.AbwesenheitTyp.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 54, Col: 62}
}
_, 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, 17, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
work, pause, overtime := day.GetAllWorkTimesVirtual(e)
templ_7745c5c3_Err = ColorDuration(work, noBorder).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = ColorDuration(pause, noBorder).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = ColorDuration(overtime, noBorder+" border-r-0").Render(ctx, templ_7745c5c3_Buffer)
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
}
if day.Date().Weekday() == time.Friday {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p class=\"col-span-full bg-neutral-300\">Wochenende</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div></content>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -270,6 +109,166 @@ func PDFReportEmploye(e models.User, workDays []models.IWorkDay, tsStart time.Ti
}) })
} }
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: 53, 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: 54, 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: 54, 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: 60, 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: 60, 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 { func ColorDuration(d time.Duration, classes string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -286,9 +285,9 @@ func ColorDuration(d time.Duration, classes string) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var14 := templ.GetChildren(ctx) templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var14 == nil { if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var14 = templ.NopComponent templ_7745c5c3_Var11 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
@@ -296,38 +295,38 @@ func ColorDuration(d time.Duration, classes string) templ.Component {
if d.Abs() < time.Minute { if d.Abs() < time.Minute {
color = "text-neutral-300" color = "text-neutral-300"
} }
var templ_7745c5c3_Var15 = []any{color + " " + classes} var templ_7745c5c3_Var12 = []any{color + " " + classes}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<p class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<p class=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var13 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String()) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 1, Col: 0} 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_Var16)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDurationFill(d, true)) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDurationFill(d, true))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 76, Col: 72} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 135, Col: 72}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -0,0 +1,29 @@
package templates
import "arbeitszeitmessung/models"
import "arbeitszeitmessung/helper"
templ TeamPresencePage(teamPresence map[models.User]bool) {
@Base()
@headerComponent()
<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>
for user, present := range teamPresence {
<div class="grid-sub">
<div class="grid-cell flex flex-row gap-2 col-span-2 md:col-span-1">
@timeGaugeComponent(helper.BoolToInt8(present)*100-1, false)
<p>{ user.Vorname } { user.Name }</p>
</div>
<div class="grid-cell col-span-2">
if present {
<span class="text-neutral-500">Anwesend</span>
} else {
<span class="text-neutral-500">Abwesend</span>
}
</div>
</div>
}
</div>
}

View File

@@ -0,0 +1,110 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.943
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

@@ -32,11 +32,11 @@ templ defaultWeekDayComponent(u models.User, day models.IWorkDay) {
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
@timeGaugeComponent(day.GetDayProgress(u), false) @timeGaugeComponent(day.GetDayProgress(u), false)
<div class="flex flex-col"> <div class="flex flex-col">
<p class=""><span class="font-bold uppercase hidden md:inline">{ day.Date().Format("Mon") }:</span> { day.Date().Format("02.01.2006") }</p> <p class=""><span class="font-bold uppercase hidden md:inline">{ helper.FormatGermanDayOfWeek(day.Date()) }:</span> { day.Date().Format("02.01.2006") }</p>
if day.IsWorkDay() { if day.IsWorkDay() {
{{ {{
workDay, _ := day.(*models.WorkDay) workDay, _ := day.(*models.WorkDay)
work, pause, _ := workDay.GetAllWorkTimesReal(u) work, pause, _ := workDay.GetTimes(u, models.WorktimeBaseDay, false)
}} }}
if !workDay.RequiresAction() { if !workDay.RequiresAction() {
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
@@ -81,7 +81,7 @@ templ weekDayComponent(user models.User, day models.WorkDay) {
templ workWeekComponent(week models.WorkWeek, onlyAccept bool) { templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
{{ {{
year, kw := week.WeekStart.ISOWeek() year, kw := week.WeekStart.ISOWeek()
progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100 progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100
}} }}
<div class="employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1 @container"> <div class="employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1 @container">
<div class="grid-cell flex flex-col max-md:bg-neutral-300 gap-2"> <div class="grid-cell flex flex-col max-md:bg-neutral-300 gap-2">

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.924 // templ: version: v0.3.943
package templates package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -151,9 +151,9 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("Mon")) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatGermanDayOfWeek(day.Date()))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 92} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 108}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -166,7 +166,7 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006")) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 136} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 152}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -179,7 +179,7 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component
if day.IsWorkDay() { if day.IsWorkDay() {
workDay, _ := day.(*models.WorkDay) workDay, _ := day.(*models.WorkDay)
work, pause, _ := workDay.GetAllWorkTimesReal(u) work, pause, _ := workDay.GetTimes(u, models.WorktimeBaseDay, false)
if !workDay.RequiresAction() { if !workDay.RequiresAction() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"flex flex-row gap-2\"><span class=\"text-accent\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"flex flex-row gap-2\"><span class=\"text-accent\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -346,7 +346,7 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component {
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
year, kw := week.WeekStart.ISOWeek() year, kw := week.WeekStart.ISOWeek()
progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100 progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<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\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err

View File

@@ -19,78 +19,16 @@ templ lineComponent() {
</div> </div>
} }
// templ workDayComponent(workDay models.WorkDay, submitted bool) {
// {{
// // work, pause := workDay.GetWorkTimeString()
// user := ctx.Value("user").(models.User)
// work, pause, overtime := workDay.GetAllWorkTimesReal(user)
// // overtime := helper.FormatDuration(workDay.CalcOvertime(user))
// justify := ""
// if len(workDay.Bookings) <= 1 {
// justify = "justify-content: center"
// }
// }}
// <div class={ "grid-sub divide-x-1 hover:bg-neutral-200 transition-colors", templ.KV("bg-neutral-100", submitted) }>
// <div class="grid-cell md:col-span-1 flex flex-row gap-2">
// @timeGaugeComponent(workDay.GetDayProgress(user), workDay.Day.Equal(time.Now().Truncate(24*time.Hour)))
// <div>
// <p class=""><span class="font-bold uppercase hidden md:inline">{ workDay.Day.Format("Mon") }:</span> { workDay.Day.Format("02.01.2006") }</p>
// if (workDay.RequiresAction()) {
// <p class="text-red-600">Bitte anpassen</p>
// } else {
// if work > 0 {
// <p class=" text-sm mt-1">Arbeitszeit:</p>
// <p class="text-accent flex flex-row items-center"><span class="icon-[material-symbols-light--nest-clock-farsight-analog-outline]"></span>{ helper.FormatDuration(work) }</p>
// }
// if pause > 0 {
// }
// <p class="text-neutral-500 flex flex-row items-center"><span class="icon-[material-symbols-light--motion-photos-paused-outline]"></span>{ helper.FormatDuration(pause) }</p>
// if overtime > 0 {
// <p class="text-neutral-500 flex flex-row items-center"><span class="icon-[material-symbols-light--more-time]"></span>{ helper.FormatDuration(overtime) }</p>
// }
// }
// </div>
// </div>
// <div class="all-booking-component flex flex-row md:col-span-3 gap-2 w-full grid-cell">
// @lineComponent()
// <form id={ "time-" + workDay.Day.Format("2006-01-02") } class="flex flex-col gap-2 group w-full justify-between" style={ justify } method="post">
// if len(workDay.Bookings) < 1 {
// <p class="text group-[.edit]:hidden">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p>
// @absenceComponent(workDay)
// @newBookingComponent(workDay)
// } else {
// @absenceComponent(workDay)
// for _, booking := range workDay.Bookings {
// @bookingComponent(booking)
// }
// if workDay.IsKurzArbeit() {
// <p>Kurzarbeit</p>
// }
// @newBookingComponent(workDay)
// }
// <input type="hidden" name="action" value="change"/> <!-- default action value for ändern button -->
// </form>
// </div>
// <div class="grid-cell">
// @changeButtonComponent("time-" + workDay.Day.Format("2006-01-02"))
// </div>
// </div>
// }
templ changeButtonComponent(id string, workDay bool) { templ changeButtonComponent(id string, workDay bool) {
{{ <button class="change-button-component btn w-auto group/button" type="button" onclick={ templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), id, workDay) }>
functionName := "editDay" <p class="hidden md:block group-[.edit]/button:hidden">Ändern</p>
if !workDay { <p class="hidden group-[.edit]/button:md:block">Absenden</p>
functionName = "editAbsence"
}
}}
<button class="btn w-auto group" type="submit" onclick={ templ.JSFuncCall(functionName, templ.JSExpression("this"), templ.JSExpression("event"), id) }>
<p class="hidden md:block group-[.edit]:hidden">Ändern</p>
<p class="hidden group-[.edit]:md:block">Absenden</p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4 md:hidden"> <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]: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="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]: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> <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> </svg>
</button> </button>
<button class="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) { templ timeGaugeComponent(progress int8, today bool) {
@@ -125,16 +63,22 @@ templ timeGaugeComponent(progress int8, today bool) {
templ newAbsenceComponent() { templ newAbsenceComponent() {
<div class="no-booking-component hidden group-[.edit]:flex flex-col gap-2 align-center "> <div class="no-booking-component hidden group-[.edit]:flex flex-col gap-2 align-center ">
<button type="button" name="absence" onclick={ templ.JSFuncCall("editAbsence", templ.JSExpression("this"), templ.JSExpression("event"), 0) } class="btn border-neutral-500"> <button type="button" name="absence" onclick={ templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), 0, false) } class="btn border-neutral-500">
Neue Abwesenheit Neue Abwesenheit
</button> </button>
</div> </div>
} }
templ absenceComponent(a *models.Absence, isKurzarbeit bool) { templ absenceComponent(a *models.Absence, isKurzarbeit bool) {
<div class="flex flex-row items-center gap-2 edit-box"> {{
<input type="hidden" name="date_from" value={ a.DateFrom.Format("2006-01-02") }/> editBox := ""
<input type="hidden" name="date_to" value={ a.DateTo.Format("2006-01-02") }/> if isKurzarbeit {
editBox = "edit-box"
}
}}
<div class={ "flex flex-row items-center gap-2", editBox }>
<input type="hidden" name="date_from" value={ a.DateFrom.Format(time.DateOnly) }/>
<input type="hidden" name="date_to" value={ a.DateTo.Format(time.DateOnly) }/>
<input type="hidden" name="aw_type" value={ a.AbwesenheitTyp.Id }/> <input type="hidden" name="aw_type" value={ a.AbwesenheitTyp.Id }/>
<input type="hidden" name="aw_id" value={ a.CounterId }/> <input type="hidden" name="aw_id" value={ a.CounterId }/>
<p class="whitespace-nowrap group-[.edit]:ml-2"> <p class="whitespace-nowrap group-[.edit]:ml-2">
@@ -145,7 +89,7 @@ templ absenceComponent(a *models.Absence, isKurzarbeit bool) {
</p> </p>
<div class="w-full"></div> <div class="w-full"></div>
if isKurzarbeit { if isKurzarbeit {
<button onclick={ templ.JSFuncCall("editAbsence", templ.JSExpression("this"), templ.JSExpression("event"), "time-"+a.Date().Format("2006-01-02")) } class="hidden btn border-0 rounded-none grow-0 w-auto group-[.edit]:inline">Bearbeiten</button> <button type="button" onclick={ templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), "time-"+a.Date().Format(time.DateOnly), false) } class="hidden btn border-0 rounded-none grow-0 w-auto group-[.edit]:inline">Bearbeiten</button>
} }
</div> </div>
} }
@@ -153,7 +97,7 @@ templ absenceComponent(a *models.Absence, isKurzarbeit bool) {
templ newBookingComponent(d *models.WorkDay) { templ newBookingComponent(d *models.WorkDay) {
<div class="new-booking-component hidden group-[.edit]:flex flex-row gap-2 items-center edit-box border-dashed"> <div class="new-booking-component hidden group-[.edit]:flex flex-row gap-2 items-center edit-box border-dashed">
<input name="timestamp" type="time" value={ time.Now().Format("15:04") } class="text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer"/> <input name="timestamp" type="time" value={ time.Now().Format("15:04") } 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={ d.Day.Format("2006-01-02") }/> <input name="date" type="hidden" value={ d.Day.Format(time.DateOnly) }/>
<div class="relative"> <div class="relative">
<select class="cursor-pointer appearance-none" name="check_in_out"> <select class="cursor-pointer appearance-none" name="check_in_out">
<option value="0" disabled>Kommen/Gehen</option> <option value="0" disabled>Kommen/Gehen</option>
@@ -176,6 +120,9 @@ templ bookingComponent(booking models.Booking) {
<input disabled name={ "booking_" + strconv.Itoa(booking.CounterId) } type="time" value={ booking.Timestamp.Format("15:04") } class="text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer"/> <input disabled name={ "booking_" + strconv.Itoa(booking.CounterId) } type="time" value={ booking.Timestamp.Format("15:04") } class="text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer"/>
{ booking.GetBookingType() } { booking.GetBookingType() }
</p> </p>
if booking.IsSubmittedAndChecked() {
<p>submitted</p>
}
</div> </div>
} }

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.924 // templ: version: v0.3.943
package templates package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -44,63 +44,6 @@ func lineComponent() templ.Component {
}) })
} }
// templ workDayComponent(workDay models.WorkDay, submitted bool) {
// {{
// // work, pause := workDay.GetWorkTimeString()
// user := ctx.Value("user").(models.User)
// work, pause, overtime := workDay.GetAllWorkTimesReal(user)
// // overtime := helper.FormatDuration(workDay.CalcOvertime(user))
// justify := ""
// if len(workDay.Bookings) <= 1 {
// justify = "justify-content: center"
// }
// }}
// <div class={ "grid-sub divide-x-1 hover:bg-neutral-200 transition-colors", templ.KV("bg-neutral-100", submitted) }>
// <div class="grid-cell md:col-span-1 flex flex-row gap-2">
// @timeGaugeComponent(workDay.GetDayProgress(user), workDay.Day.Equal(time.Now().Truncate(24*time.Hour)))
// <div>
// <p class=""><span class="font-bold uppercase hidden md:inline">{ workDay.Day.Format("Mon") }:</span> { workDay.Day.Format("02.01.2006") }</p>
// if (workDay.RequiresAction()) {
// <p class="text-red-600">Bitte anpassen</p>
// } else {
// if work > 0 {
// <p class=" text-sm mt-1">Arbeitszeit:</p>
// <p class="text-accent flex flex-row items-center"><span class="icon-[material-symbols-light--nest-clock-farsight-analog-outline]"></span>{ helper.FormatDuration(work) }</p>
// }
// if pause > 0 {
// }
// <p class="text-neutral-500 flex flex-row items-center"><span class="icon-[material-symbols-light--motion-photos-paused-outline]"></span>{ helper.FormatDuration(pause) }</p>
// if overtime > 0 {
// <p class="text-neutral-500 flex flex-row items-center"><span class="icon-[material-symbols-light--more-time]"></span>{ helper.FormatDuration(overtime) }</p>
// }
// }
// </div>
// </div>
// <div class="all-booking-component flex flex-row md:col-span-3 gap-2 w-full grid-cell">
// @lineComponent()
// <form id={ "time-" + workDay.Day.Format("2006-01-02") } class="flex flex-col gap-2 group w-full justify-between" style={ justify } method="post">
// if len(workDay.Bookings) < 1 {
// <p class="text group-[.edit]:hidden">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p>
// @absenceComponent(workDay)
// @newBookingComponent(workDay)
// } else {
// @absenceComponent(workDay)
// for _, booking := range workDay.Bookings {
// @bookingComponent(booking)
// }
// if workDay.IsKurzArbeit() {
// <p>Kurzarbeit</p>
// }
// @newBookingComponent(workDay)
// }
// <input type="hidden" name="action" value="change"/> <!-- default action value for ändern button -->
// </form>
// </div>
// <div class="grid-cell">
// @changeButtonComponent("time-" + workDay.Day.Format("2006-01-02"))
// </div>
// </div>
// }
func changeButtonComponent(id string, workDay bool) templ.Component { func changeButtonComponent(id string, workDay bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -122,25 +65,37 @@ func changeButtonComponent(id string, workDay bool) templ.Component {
templ_7745c5c3_Var2 = templ.NopComponent templ_7745c5c3_Var2 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), id, workDay))
functionName := "editDay"
if !workDay {
functionName = "editAbsence"
}
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall(functionName, templ.JSExpression("this"), templ.JSExpression("event"), id))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<button class=\"btn w-auto group\" type=\"submit\" onclick=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<button class=\"change-button-component btn w-auto group/button\" type=\"button\" onclick=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 templ.ComponentScript = templ.JSFuncCall(functionName, templ.JSExpression("this"), templ.JSExpression("event"), id) 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) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3.Call)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><p class=\"hidden md:block group-[.edit]:hidden\">Ändern</p><p class=\"hidden group-[.edit]:md:block\">Absenden</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]: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]: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>") 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\">Absenden</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=\"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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -164,9 +119,9 @@ func timeGaugeComponent(progress int8, today bool) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx) templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil { if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var4 = templ.NopComponent templ_7745c5c3_Var5 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
@@ -189,65 +144,65 @@ func timeGaugeComponent(progress int8, today bool) templ.Component {
break break
} }
if today { if today {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"flex-start flex w-2 h-full overflow-hidden rounded-full bg-neutral-300 print:hidden\">") 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 = []any{"flex w-full items-center justify-center overflow-hidden rounded-full", bgColor} 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_Var5...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).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_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" style=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("height: %d%%", int(progress))) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var6).String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 119, Col: 149} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"></div></div>") 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
var templ_7745c5c3_Var8 = []any{"w-2 h-full bg-accent rounded-md flex-shrink-0", bgColor} 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_Var8...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String()) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 1, Col: 0} 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_Var9)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -272,29 +227,29 @@ func newAbsenceComponent() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var10 := templ.GetChildren(ctx) templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil { if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var10 = templ.NopComponent templ_7745c5c3_Var11 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"no-booking-component hidden group-[.edit]:flex flex-col gap-2 align-center \">") 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("editAbsence", templ.JSExpression("this"), templ.JSExpression("event"), 0)) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<button type=\"button\" name=\"absence\" onclick=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<button type=\"button\" name=\"absence\" onclick=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 templ.ComponentScript = templ.JSFuncCall("editAbsence", templ.JSExpression("this"), templ.JSExpression("event"), 0) 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_Var11.Call) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12.Call)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" class=\"btn border-neutral-500\">Neue Abwesenheit</button></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"btn border-neutral-500\">Neue Abwesenheit</button></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -318,123 +273,146 @@ func absenceComponent(a *models.Absence, isKurzarbeit bool) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var12 := templ.GetChildren(ctx) templ_7745c5c3_Var13 := templ.GetChildren(ctx)
if templ_7745c5c3_Var12 == nil { if templ_7745c5c3_Var13 == nil {
templ_7745c5c3_Var12 = templ.NopComponent templ_7745c5c3_Var13 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"flex flex-row items-center gap-2 edit-box\"><input type=\"hidden\" name=\"date_from\" value=\"")
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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 string templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"")
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateFrom.Format("2006-01-02"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 136, Col: 79}
}
_, 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, 14, "\"> <input type=\"hidden\" name=\"date_to\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateTo.Format("2006-01-02"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 137, Col: 75}
}
_, 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, 15, "\"> <input type=\"hidden\" name=\"aw_type\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 string var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(a.AbwesenheitTyp.Id) templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 138, Col: 65} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"> <input type=\"hidden\" name=\"aw_id\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"><input type=\"hidden\" name=\"date_from\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(a.CounterId) templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateFrom.Format(time.DateOnly))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 139, Col: 55} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 80, Col: 80}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"><p class=\"whitespace-nowrap group-[.edit]:ml-2\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"> <input type=\"hidden\" name=\"date_to\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(a.AbwesenheitTyp.Name) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateTo.Format(time.DateOnly))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 141, Col: 26} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 81, Col: 76}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"> <input type=\"hidden\" name=\"aw_type\" value=\"")
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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var18 string var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateTo.Format("02.01.2006")) templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(a.AbwesenheitTyp.Id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 143, Col: 70} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 82, Col: 65}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"> <input type=\"hidden\" name=\"aw_id\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(a.CounterId)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 83, Col: 55}
}
_, 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, 20, "\"><p class=\"whitespace-nowrap group-[.edit]:ml-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(a.AbwesenheitTyp.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 85, Col: 26}
}
_, 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, 21, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if a.IsMultiDay() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"text-neutral-500\">bis ")
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("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 87, Col: 70}
}
_, 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, 23, "</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</p><div class=\"w-full\"></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p><div class=\"w-full\"></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if isKurzarbeit { if isKurzarbeit {
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("editAbsence", templ.JSExpression("this"), templ.JSExpression("event"), "time-"+a.Date().Format("2006-01-02"))) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<button onclick=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<button type=\"button\" onclick=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var19 templ.ComponentScript = templ.JSFuncCall("editAbsence", templ.JSExpression("this"), templ.JSExpression("event"), "time-"+a.Date().Format("2006-01-02")) var templ_7745c5c3_Var22 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_Var19.Call) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22.Call)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" class=\"hidden btn border-0 rounded-none grow-0 w-auto group-[.edit]:inline\">Bearbeiten</button>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -458,58 +436,58 @@ func newBookingComponent(d *models.WorkDay) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var20 := templ.GetChildren(ctx) templ_7745c5c3_Var23 := templ.GetChildren(ctx)
if templ_7745c5c3_Var20 == nil { if templ_7745c5c3_Var23 == nil {
templ_7745c5c3_Var20 = templ.NopComponent templ_7745c5c3_Var23 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"new-booking-component hidden group-[.edit]:flex flex-row gap-2 items-center edit-box border-dashed\"><input name=\"timestamp\" type=\"time\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"new-booking-component hidden group-[.edit]:flex flex-row gap-2 items-center edit-box border-dashed\"><input name=\"timestamp\" type=\"time\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var21 string var templ_7745c5c3_Var24 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(time.Now().Format("15:04")) templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(time.Now().Format("15:04"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 155, Col: 72} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 99, Col: 72}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" 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=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var22 string var templ_7745c5c3_Var25 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(d.Day.Format("2006-01-02")) templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(d.Day.Format(time.DateOnly))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 156, Col: 69} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 100, Col: 70}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"><div class=\"relative\"><select class=\"cursor-pointer appearance-none\" name=\"check_in_out\"><option value=\"0\" disabled>Kommen/Gehen</option> <option value=\"3\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\"><div class=\"relative\"><select class=\"cursor-pointer appearance-none\" name=\"check_in_out\"><option value=\"0\" disabled>Kommen/Gehen</option> <option value=\"3\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(d.Bookings) > 0 && d.Bookings[len(d.Bookings)-1].CheckInOut%2 == 0 { if len(d.Bookings) > 0 && d.Bookings[len(d.Bookings)-1].CheckInOut%2 == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">Kommen</option> <option value=\"4\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, ">Kommen</option> <option value=\"4\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(d.Bookings) > 0 && d.Bookings[len(d.Bookings)-1].CheckInOut%2 == 1 { if len(d.Bookings) > 0 && d.Bookings[len(d.Bookings)-1].CheckInOut%2 == 1 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, ">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>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ">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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -533,64 +511,74 @@ func bookingComponent(booking models.Booking) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var23 := templ.GetChildren(ctx) templ_7745c5c3_Var26 := templ.GetChildren(ctx)
if templ_7745c5c3_Var23 == nil { if templ_7745c5c3_Var26 == nil {
templ_7745c5c3_Var23 = templ.NopComponent templ_7745c5c3_Var26 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div><p class=\"text-neutral-500 edit-box\"><span class=\"text-black group-[.edit]:hidden inline\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<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_Var24 string
templ_7745c5c3_Var24, 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: 175, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</span> <input disabled name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, 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: 176, Col: 70}
}
_, 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, 34, "\" 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(booking.Timestamp.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 176, Col: 126}
}
_, 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, 35, "\" class=\"text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer\"> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var27 string var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(booking.GetBookingType()) templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(booking.Timestamp.Format("15:04"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 177, Col: 29} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 119, Col: 91}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</span> <input disabled name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, 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: 120, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" type=\"time\" value=\"")
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: 120, Col: 126}
}
_, 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, 38, "\" 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_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(booking.GetBookingType())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 121, Col: 29}
}
_, 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, 39, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if booking.IsSubmittedAndChecked() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<p>submitted</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -614,12 +602,12 @@ func LegendComponent() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var28 := templ.GetChildren(ctx) templ_7745c5c3_Var31 := templ.GetChildren(ctx)
if templ_7745c5c3_Var28 == nil { if templ_7745c5c3_Var31 == nil {
templ_7745c5c3_Var28 = templ.NopComponent templ_7745c5c3_Var31 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<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_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -79,7 +79,7 @@ templ inputForm() {
<div class="grid-cell flex flex-row items-end"> <div class="grid-cell flex flex-row items-end">
<div class="flex flex-row gap-2 w-full"> <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="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]"><span class="icon-[material-symbols-light--delete-outline]"></span></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>
</div> </div>
</form> </form>
@@ -93,17 +93,18 @@ templ defaultDayComponent(day models.IWorkDay) {
justify = "justify-between" justify = "justify-between"
} }
}} }}
<div class={ "grid-sub divide-x-1 hover:bg-neutral-200 transition-colors" }> <div class={ "grid-sub divide-x-1 hover:bg-neutral-200 transition-colors group" }>
<div class="grid-cell md:col-span-1 flex flex-row gap-2"> <div class="grid-cell md:col-span-1 flex flex-row gap-2">
@timeGaugeComponent(day.GetDayProgress(user), day.Date().Equal(time.Now().Truncate(24*time.Hour))) @timeGaugeComponent(day.GetDayProgress(user), day.Date().Equal(time.Now().Truncate(24*time.Hour)))
<div> <div>
<p> <p>
<span class="font-bold uppercase hidden md:inline">{ day.Date().Format("Mon") }:</span> { day.Date().Format("02.01.2006") } <span class="font-bold uppercase hidden md:inline">{ helper.FormatGermanDayOfWeek(day.Date()) }:</span> { day.Date().Format("02.01.2006") }
</p> </p>
if day.IsWorkDay() { if day.IsWorkDay() {
{{ {{
workDay, _ := day.(*models.WorkDay) workDay, _ := day.(*models.WorkDay)
work, pause, overtime := workDay.GetAllWorkTimesVirtual(user) work, pause, overtime := workDay.GetTimes(user, models.WorktimeBaseDay, true)
work = workDay.GetWorktime(user, models.WorktimeBaseDay, false)
}} }}
if day.RequiresAction() { if day.RequiresAction() {
<p class="text-red-600">Bitte anpassen</p> <p class="text-red-600">Bitte anpassen</p>
@@ -125,18 +126,18 @@ templ defaultDayComponent(day models.IWorkDay) {
} }
</div> </div>
</div> </div>
<div class="all-booking-component grid-cell flex flex-row md:col-span-3 gap-2 w-full"> <div class="all-booking-component grid-cell flex flex-row md:col-span-3 col-span-2 gap-2 w-full">
@lineComponent() @lineComponent()
<form id={ "time-" + day.Date().Format("2006-01-02") } class={ "flex flex-col gap-2 group w-full", justify } method="post"> <form id={ "time-" + day.Date().Format(time.DateOnly) } class={ "flex flex-col gap-2 w-full", justify } method="post">
@newAbsenceComponent()
if day.IsWorkDay() { if day.IsWorkDay() {
{{ {{
workDay, _ := day.(*models.WorkDay) workDay, _ := day.(*models.WorkDay)
}} }}
@newAbsenceComponent()
if len(workDay.Bookings) < 1 { if len(workDay.Bookings) < 1 {
<p class="text group-[.edit]:hidden">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p> <p class="text group-[.edit]:hidden">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p>
} }
if workDay.IsKurzArbeit() { if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 {
@absenceComponent(workDay.GetKurzArbeit(), true) @absenceComponent(workDay.GetKurzArbeit(), true)
} }
for _, booking := range workDay.Bookings { for _, booking := range workDay.Bookings {
@@ -149,17 +150,18 @@ templ defaultDayComponent(day models.IWorkDay) {
}} }}
@absenceComponent(absentDay, false) @absenceComponent(absentDay, false)
} }
<input type="hidden" name="action" value="change"/> <!-- default action value for ändern button -->
</form> </form>
</div> </div>
<div class="grid-cell"> <div class="grid-cell flex flex-row gap-2 items-end">
@changeButtonComponent("time-"+day.Date().Format("2006-01-02"), day.IsWorkDay()) @changeButtonComponent("time-"+day.Date().Format(time.DateOnly), day.IsWorkDay())
</div> </div>
</div> </div>
} }
templ absentInput(a models.Absence) { templ absentInput(a models.Absence) {
<input type="hidden" name="date_from" value={ a.DateFrom.Format("2006-01-02") }/> <input type="hidden" name="date_from" value={ a.DateFrom.Format(time.DateOnly) }/>
<input type="hidden" name="date_to" value={ a.DateTo.Format("2006-01-02") }/> <input type="hidden" name="date_to" value={ a.DateTo.Format(time.DateOnly) }/>
<input type="hidden" name="aw_type" value={ a.AbwesenheitTyp.Id }/> <input type="hidden" name="aw_type" value={ a.AbwesenheitTyp.Id }/>
<input type="hidden" name="aw_id" value={ a.CounterId }/> <input type="hidden" name="aw_id" value={ a.CounterId }/>
} }

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.924 // templ: version: v0.3.943
package templates package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -203,7 +203,7 @@ func inputForm() templ.Component {
return templ_7745c5c3_Err 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]\"><span class=\"icon-[material-symbols-light--delete-outline]\"></span></button></div></div></form>") 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>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -238,7 +238,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
if day.IsWorkDay() && len(day.(*models.WorkDay).Bookings) > 1 { if day.IsWorkDay() && len(day.(*models.WorkDay).Bookings) > 1 {
justify = "justify-between" justify = "justify-between"
} }
var templ_7745c5c3_Var10 = []any{"grid-sub divide-x-1 hover:bg-neutral-200 transition-colors"} 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...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
@@ -269,9 +269,9 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var12 string var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("Mon")) templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatGermanDayOfWeek(day.Date()))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 82} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -284,7 +284,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
var templ_7745c5c3_Var13 string var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006")) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 126} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -297,7 +297,8 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
if day.IsWorkDay() { if day.IsWorkDay() {
workDay, _ := day.(*models.WorkDay) workDay, _ := day.(*models.WorkDay)
work, pause, overtime := workDay.GetAllWorkTimesVirtual(user) work, pause, overtime := workDay.GetTimes(user, models.WorktimeBaseDay, true)
work = workDay.GetWorktime(user, models.WorktimeBaseDay, false)
if day.RequiresAction() { if day.RequiresAction() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<p class=\"text-red-600\">Bitte anpassen</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<p class=\"text-red-600\">Bitte anpassen</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -312,7 +313,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(work)) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(work))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 113, Col: 155} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 114, Col: 155}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -335,7 +336,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
var templ_7745c5c3_Var15 string var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(pause)) templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(pause))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 116, Col: 173} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 117, Col: 173}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -358,7 +359,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(overtime)) templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(overtime))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 121, Col: 41} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 122, Col: 41}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -371,7 +372,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
} }
} }
} }
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 gap-2 w-full\">") 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -379,7 +380,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 = []any{"flex flex-col gap-2 group w-full", justify} var templ_7745c5c3_Var17 = []any{"flex flex-col gap-2 w-full", justify}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
@@ -389,9 +390,9 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var18 string var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("time-" + day.Date().Format("2006-01-02")) templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("time-" + day.Date().Format(time.DateOnly))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 130, Col: 55} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 131, Col: 56}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -414,24 +415,28 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if day.IsWorkDay() {
workDay, _ := day.(*models.WorkDay)
templ_7745c5c3_Err = newAbsenceComponent().Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = newAbsenceComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if day.IsWorkDay() { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ")
if templ_7745c5c3_Err != nil {
workDay, _ := day.(*models.WorkDay) return templ_7745c5c3_Err
}
if len(workDay.Bookings) < 1 { if len(workDay.Bookings) < 1 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<p class=\"text group-[.edit]:hidden\">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<p class=\"text group-[.edit]:hidden\">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if workDay.IsKurzArbeit() { if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 {
templ_7745c5c3_Err = absenceComponent(workDay.GetKurzArbeit(), true).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = absenceComponent(workDay.GetKurzArbeit(), true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
@@ -443,7 +448,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -459,15 +464,15 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</form></div><div class=\"grid-cell\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = changeButtonComponent("time-"+day.Date().Format("2006-01-02"), day.IsWorkDay()).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = changeButtonComponent("time-"+day.Date().Format(time.DateOnly), day.IsWorkDay()).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -496,59 +501,59 @@ func absentInput(a models.Absence) templ.Component {
templ_7745c5c3_Var20 = templ.NopComponent templ_7745c5c3_Var20 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<input type=\"hidden\" name=\"date_from\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<input type=\"hidden\" name=\"date_from\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var21 string var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateFrom.Format("2006-01-02")) templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateFrom.Format(time.DateOnly))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 161, Col: 78} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 163, Col: 79}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\"> <input type=\"hidden\" name=\"date_to\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\"> <input type=\"hidden\" name=\"date_to\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var22 string var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateTo.Format("2006-01-02")) templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateTo.Format(time.DateOnly))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 162, Col: 74} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 164, Col: 75}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\"> <input type=\"hidden\" name=\"aw_type\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"> <input type=\"hidden\" name=\"aw_type\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var23 string var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(a.AbwesenheitTyp.Id) templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(a.AbwesenheitTyp.Id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 163, Col: 64} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 165, Col: 64}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"> <input type=\"hidden\" name=\"aw_id\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\"> <input type=\"hidden\" name=\"aw_id\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var24 string var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(a.CounterId) templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(a.CounterId)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 164, Col: 54} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 166, Col: 54}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

28
DB/initdb/01_schema.sql Normal file → Executable file
View File

@@ -22,7 +22,7 @@ COMMENT ON COLUMN "anwesenheit"."geraet_id" IS 'ID des Lesegerätes';
DROP TABLE IF EXISTS "s_anwesenheit_typen"; DROP TABLE IF EXISTS "s_anwesenheit_typen";
CREATE TABLE "s_anwesenheit_typen" ( CREATE TABLE "s_anwesenheit_typen" (
"anwesenheit_id" int2 PRIMARY KEY, "anwesenheit_id" int2 PRIMARY KEY,
"anwesenheit_name" varchar(255) "anwesenheit_name" varchar(255) NOT NULL
); );
-- ---------------------------- -- ----------------------------
@@ -78,30 +78,34 @@ EXECUTE FUNCTION update_zuletzt_geandert();
DROP TABLE IF EXISTS "wochen_report"; DROP TABLE IF EXISTS "wochen_report";
CREATE TABLE "wochen_report" ( CREATE TABLE "wochen_report" (
"id" serial PRIMARY KEY, "id" serial PRIMARY KEY,
"personal_nummer" int4, "personal_nummer" int4 NOT NULL,
"woche_start" date, "woche_start" date NOT NULL,
"bestaetigt" bool DEFAULT FALSE, "bestaetigt" bool DEFAULT FALSE,
"arbeitszeit" interval, "arbeitszeit" interval NOT NULL,
"ueberstunden" interval, "ueberstunden" interval NOT NULL,
"anwesenheiten" int ARRAY,
"abwesenheiten" int ARRAY,
UNIQUE ("personal_nummer", "woche_start") UNIQUE ("personal_nummer", "woche_start")
); );
DROP TABLE IF EXISTS "abwesenheit"; DROP TABLE IF EXISTS "abwesenheit";
CREATE TABLE "abwesenheit" ( CREATE TABLE "abwesenheit" (
"counter_id" bigserial PRIMARY KEY, "counter_id" bigserial PRIMARY KEY,
"card_uid" varchar(255), "card_uid" varchar(255) NOT NULL,
"abwesenheit_typ" int2, "abwesenheit_typ" int2 NOT NULL,
"datum_from" timestamptz DEFAULT NOW()::DATE, "datum_from" timestamptz DEFAULT NOW()::DATE NOT NULL,
"datum_to" timestamptz "datum_to" timestamptz NOT NULL
); );
DROP TABLE IF EXISTS "s_abwesenheit_typen"; DROP TABLE IF EXISTS "s_abwesenheit_typen";
CREATE TABLE "s_abwesenheit_typen" ( CREATE TABLE "s_abwesenheit_typen" (
"abwesenheit_id" int2 PRIMARY KEY, "abwesenheit_id" int2 PRIMARY KEY NOT NULL,
"abwesenheit_name" varchar(255), "abwesenheit_name" varchar(255) NOT NULL,
"arbeitszeit_equivalent" float4 "arbeitszeit_equivalent" float4 NOT NULL
); );
COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; -1=Arbeitszeit auffüllen; <=1 - 100 => Arbeitszeit pro Tag prozentual';
-- Adds crypto extension -- Adds crypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pgcrypto;

0
DB/initdb/02_sample_data.sql Normal file → Executable file
View File

0
DB/initdb/03_create_user.sh Normal file → Executable file
View File

View File

@@ -1,11 +0,0 @@
POSTGRES_USER=root
POSTGRES_PASSWORD=very_secure
POSTGRES_API_USER=api_nutzer
POSTGRES_API_PASS=password
POSTGRES_PATH=../DB
POSTGRES_DB=arbeitszeitmessung
EXPOSED_PORT=8000
TZ=Europe/Berlin
PGTZ=Europe/Berlin
API_TOKEN=dont_access
EMPTY_DAYS=false

View File

@@ -1,12 +1,6 @@
name: arbeitszeitmessung-dev name: arbeitszeitmessung-dev
services: services:
db: db:
image: postgres:16
restart: unless-stopped
env_file:
- .env
environment:
PGDATA: /var/lib/postgresql/data/pg_data
volumes: volumes:
- ${POSTGRES_PATH}:/var/lib/postgresql/data - ${POSTGRES_PATH}:/var/lib/postgresql/data
# - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d # - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
@@ -19,21 +13,8 @@ services:
ports: ports:
- 8001:8080 - 8001:8080
backend: backend:
image: git.letsstein.de/tom/arbeitszeitmessung
restart: unless-stopped
env_file:
- .env
environment: environment:
POSTGRES_HOST: db
POSTGRES_DB: ${POSTGRES_DB}
EXPOSED_PORT: ${EXPOSED_PORT}
NO_CORS: true NO_CORS: true
ports:
- ${EXPOSED_PORT}:8080
volumes:
- ../logs:/app/Backend/logs
depends_on:
- db
swagger: swagger:
image: swaggerapi/swagger-ui image: swaggerapi/swagger-ui

View File

@@ -6,28 +6,31 @@ services:
env_file: env_file:
- .env - .env
environment: environment:
POSTGRES_USER: ${POSTGRES_USER} PGTZ: ${TZ}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
PGDATA: /var/lib/postgresql/data/pg_data PGDATA: /var/lib/postgresql/data/pg_data
volumes: volumes:
- ${POSTGRES_PATH}:/var/lib/postgresql/data - ${POSTGRES_PATH}:/var/lib/postgresql/data
- ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
ports: ports:
- 5432:5432 - ${POSTGRES_PORT}:5432
backend: backend:
image: git.letsstein.de/tom/arbeitszeitmessung image: git.letsstein.de/tom/arbeitszeitmessung-webserver
env_file: env_file:
- .env - .env
environment: environment:
POSTGRES_HOST: db POSTGRES_HOST: db
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
EXPOSED_PORT: ${EXPOSED_PORT}
ports: ports:
- ${EXPOSED_PORT}:8080 - ${WEB_PORT}:8080
depends_on: depends_on:
- db - db
- document-creator
volumes: volumes:
- ../logs:/app/Backend/logs - ../logs:/app/Backend/logs
restart: unless-stopped restart: unless-stopped
document-creator:
image: git.letsstein.de/tom/arbeitszeitmessung-doc-creator
container_name: ${TYPST_CONTAINER}
restart: unless-stopped

12
Docker/env.example Normal file
View File

@@ -0,0 +1,12 @@
POSTGRES_USER=root # Postgres ADMIN Nutzername
POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort
POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung)
POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung)
POSTGRES_PATH=../DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...)
POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name
POSTGRES_PORT=127.0.0.1:5432 # Postgres Port will not be exposed by default.
TZ=Europe/Berlin # Zeitzone
PGTZ=Europe/Berlin # Zeitzone
API_TOKEN=dont_access # API Token für ESP Endpoints
WEB_PORT=8000 # Port from which Arbeitszeitmessung should be accessable regex:^[0-9]{1,5}$
TYPST_CONTAINER=arbeitszeitmessung-doc-creator # Name of the pdf compiler container

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,97 @@
#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, .875fr, 1.25fr),
fill: (x, y) =>
if y == 0 { oklch(87%, 0, 0deg) },
table-header(
[Datum], [Kommen], [Gehen], [Arbeitsart], [Stunden], [Kurzarbeit], [Pause], [Überstunden]
),
.. for day in days {
(
[#day.Date],
if day.DayParts.len() == 0{
table.cell(colspan: 3)[Keine Buchungen]
}else if day.DayParts.len() == 1 and 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 {
(
if Zeit.IsWorkDay{
(
table.cell()[#Zeit.BookingFrom],
table.cell()[#Zeit.BookingTo],
table.cell()[#Zeit.WorkType],
)
}else{
(table.cell(colspan: 3)[#Zeit.WorkType],)
}
)
},
)
]
},
[#day.Worktime],
[#day.Kurzarbeit],
[#day.Pausetime],
[#day.Overtime],
)
if day.IsFriday {
( table.cell(colspan: 8, 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],
[Kurzarbeit :], table.cell(align: left)[#meta.Kurzarbeit],
[Überstunden :], table.cell(align: left)[#meta.Overtime],
[Überstunden lfd. :],table.cell(align: left)[#meta.OvertimeTotal],
table.hline(start: 0, end: 2),
)
}

View File

@@ -44,7 +44,7 @@ generateFrontend:
backend: generateFrontend login_registry backend: generateFrontend login_registry
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:latest Backend --load #--push 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:${GIT_COMMIT} Backend //--push # docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:${GIT_COMMIT} Backend //--push
test: test:
@@ -52,3 +52,11 @@ test:
scan: test scan: test
$(MAKE) -C Backend scan $(MAKE) -C Backend scan
Docker/.env:
cp Docker/env.example Docker/.env
echo "Konfigurations Datei erstellt (./Docker/.env), bitte zuerst ausfüllen und danach erneut 'make install' ausführen"
exit 0
install: Docker/.env
echo "Install"

View File

@@ -1,5 +1,7 @@
# Arbeitszeitmessung # Arbeitszeitmessung
[![Quality Gate Status](https://sonar.letsstein.de/api/project_badges/measure?project=arbeitszeitmessung&metric=alert_status&token=sqb_253028eff30aff24f32b437cd6c484c511b5c33f)](https://sonar.letsstein.de/dashboard?id=arbeitszeitmessung)
bis jetzt ein einfaches Backend mit PostgreSQL Datenbank und GO Webserver um Arbeitszeitbuchungen per HTTP PUT einzufügen bis jetzt ein einfaches Backend mit PostgreSQL Datenbank und GO Webserver um Arbeitszeitbuchungen per HTTP PUT einzufügen
## Installation ## Installation

16
db.sql
View File

@@ -176,15 +176,15 @@ sample_bookings AS (
(d.work_date + make_time(16, floor(random()*50)::int, 0))::timestamptz AS ts, (d.work_date + make_time(16, floor(random()*50)::int, 0))::timestamptz AS ts,
1 AS anwesenheit_typ 1 AS anwesenheit_typ
FROM days d FROM days d
), )
ins_anw AS (
-- insert only bookings up to now (prevents future times on today) -- insert only bookings up to now (prevents future times on today)
INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id) INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id)
SELECT ts, card_uid, check_in_out, geraet_id SELECT ts, card_uid, check_in_out, geraet_id
FROM sample_bookings FROM sample_bookings
WHERE ts <= NOW() WHERE ts <= NOW()
RETURNING 1 RETURNING 1;
)
-- now insert absences (uses the same days CTE) -- now insert absences (uses the same days CTE)
INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum) INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum)
SELECT SELECT
@@ -247,15 +247,13 @@ all_bookings AS (
SELECT * FROM base_bookings SELECT * FROM base_bookings
UNION ALL UNION ALL
SELECT * FROM pause_bookings SELECT * FROM pause_bookings
), )
ins_anw AS (
INSERT INTO anwesenheit ("timestamp", "card_uid", "check_in_out", "geraet_id", "anwesenheit_typ") INSERT INTO anwesenheit ("timestamp", "card_uid", "check_in_out", "geraet_id", "anwesenheit_typ")
SELECT ts, card_uid, check_in_out, geraet_id, 1 as anwesenheit_typ SELECT ts, card_uid, check_in_out, geraet_id, 1 as anwesenheit_typ
FROM all_bookings FROM all_bookings
WHERE ts <= NOW() WHERE ts <= NOW()
ORDER BY work_date, ts ORDER BY work_date, ts;
RETURNING 1
)
INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum) INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum)
SELECT SELECT
d.card_uid, d.card_uid,

108
install.sh Executable file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -e
envFile=Docker/.env
envExample=Docker/env.example
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
curl -fsSL https://get.docker.com | sh
else
echo "Docker is required. Exiting."
exit 1
fi
else
echo "Docker is already installed."
fi
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
echo "Preparing .env file..."
if [ ! -f $envFile ]; then
if [ -f $envExample ]; then
echo ".env not found. Creating interactively from .env.example."
> $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-)
# 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')
regex=""
if [[ "$comment" =~ regex:(.*)$ ]]; then
regex="${BASH_REMATCH[1]}"
fi
comment=$(printf "%s" "$comment" | sed 's/ regex:.*//')
while true; do
if [ -z "$comment" ]; then
printf "Value for $key - $comment (default: $default_value"
else
printf "Value for $key (default: $default_value"
fi
if [ -n "$regex" ]; then
printf ", must match: %s" "$regex"
fi
printf "):\n"
read user_input < /dev/tty
# empty input -> take default
[ -z "$user_input" ] && user_input="$default_value"
printf "\e[A$user_input\n"
# validate
if [ -n "$regex" ]; then
if [[ "$user_input" =~ $regex ]]; then
echo "$key=$user_input" >> $envFile
break
else
printf "Invalid value. Does not match regex: %s\n" "$regex"
continue
fi
else
echo "$key=$user_input" >> $envFile
break
fi
done
done < $envExample
echo ".env created."
else
echo "No .env or .env.example found."
echo "Creating an empty .env file for manual editing."
touch $envFile
fi
else
echo "Using existing .env. (found at $envFile)"
fi
echo "Start containers with docker compose up -d? [y/N]"
read -r start_containers
if [[ "$start_containers" =~ ^[Yy]$ ]]; then
cd Docker
mkdir ../logs
docker compose up -d
echo "Containers started."
else
echo "You can start them manually with: docker compose up -d"
fi

View File

@@ -0,0 +1,4 @@
-- reverse: modify "wochen_report" table
ALTER TABLE "wochen_report" DROP COLUMN "abwesenheiten", DROP COLUMN "anwesenheiten", ALTER COLUMN "arbeitszeit" DROP NOT NULL, ALTER COLUMN "ueberstunden" DROP NOT NULL, ALTER COLUMN "woche_start" DROP NOT NULL;
-- reverse: modify "abwesenheit" table
ALTER TABLE "abwesenheit" ALTER COLUMN "datum_to" DROP NOT NULL;

View File

@@ -0,0 +1,4 @@
-- modify "abwesenheit" table
ALTER TABLE "abwesenheit" ALTER COLUMN "datum_to" SET NOT NULL;
-- modify "wochen_report" table
ALTER TABLE "wochen_report" ALTER COLUMN "woche_start" SET NOT NULL, ALTER COLUMN "ueberstunden" SET NOT NULL, ALTER COLUMN "arbeitszeit" SET NOT NULL, ADD COLUMN "anwesenheiten" integer[] NULL, ADD COLUMN "abwesenheiten" integer[] NULL;

View File

@@ -1,15 +1,9 @@
h1:3AxgD8mnu/F+JGtJu9FZvA9Ro0UUtGPgyjskKtfTYUQ= h1:gE7ikkZS7bQbedAjVspQKftSo5ODij2eiQGWbfQEYmI=
20250901201159_initial.down.sql h1:cmF5CvNGqEfcmbRgiqaqDWERdNNRaMzarbNLJ/Y35o4= 20250901201159_initial.up.sql h1:Mb1RlVdFvcxqU9HrSK6oNeURqFa3O4KzB3rDa+6+3gc=
20250901201159_initial.up.sql h1:Yrak/+wfQ4Tu/dVR/cUZ/75DlAcv4G/OJXDqpgSw47U= 20250901201250_control_tables.up.sql h1:a5LATgR/CRiC4GsqxkJ94TyJOxeTcW74eCnodIy+c1E=
20250901201250_control_tables.down.sql h1:f/KmhO9pOI45J8ZRjFonvD3CypB+rOoGOPN2WMFHvOw= 20250901201710_triggers_extension.up.sql h1:z9b6Hk9btE2Ns4mU7B16HjvYBP6EEwHAXVlvPpkn978=
20250901201250_control_tables.up.sql h1:of5E07p0N1aen9CdQNEOrO7ffbKZC6kp4oK5KPzU9+g= 20250903221313_overtime.up.sql h1:t/B435ShW5ZEnzC81jRABWVZ5gNm7tPZPnOO6/ZY6ow=
20250901201710_triggers_extension.down.sql h1:a9va3FSfHBWzODJSJO+ywNa2hiZwjG/vmvYGb3L1lnM= 20250903233030_non_null_contraints.up.sql h1:YKeYgazfh+jPyh7hFT/pV+By8eHnk1taXnlgSLyXSA0=
20250901201710_triggers_extension.up.sql h1:nUBPd2eDssi/TwMVF/nOJkIM5rUM0iINdg1K9pZRZN0= 20250904114004_intervals.up.sql h1:gDdN8cJ4xH1vQhAbbhqD5lwdyEO1N9EIqEYkmWGiWIU=
20250903221313_overtime.down.sql h1:X+jJESqcZ6ZTd2H563z6kRaXb4dn4sA02D3ck2795v8= 20250916093608_kurzarbeit.up.sql h1:yDAAMLyUXz6b7+MI6XK/HZMPzutKoT2NNNOCjFaqSts=
20250903221313_overtime.up.sql h1:C3DSiNVpe9v0Un1DEQ0lsy5yToR8iqcggv91GSr6tRE= 20251013212224_buchungs_array.up.sql h1:mbhvnwMUkEFFQQ41NC47auqxbtvNkztziWvpLDFm6tA=
20250903233030_non_null_contraints.down.sql h1:42TZzPsji2Ze50k6sLwgIuNo4Trk3m3ni/aIfQJ97dE=
20250903233030_non_null_contraints.up.sql h1:k6zR5YNSAP4fo5QEc58KZ0LxvEz1nl0X/AAcZ+TG3I4=
20250904114004_intervals.down.sql h1:SquJAPinzFIRN6fJjLLIRsz59Tyr4RwGiGuOFI/N1SQ=
20250904114004_intervals.up.sql h1:AFqncTGOiEZVBbhWFqN2zlQ7DyhybB5wJr6a36Atk1E=
20250916093608_kurzarbeit.down.sql h1:ljM1a1pQCxOQiXRaXU04GC4V9yy2y20x5eUNQ/zyx+o=
20250916093608_kurzarbeit.up.sql h1:pTiw0VfGaf26mhJg4wf98Fqwn1kShJ+PiN2PiM4q1kk=