64 Commits

Author SHA1 Message Date
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
7e27c944f3 updated time editing ui
Some checks failed
Tests / Run Go Tests (push) Failing after 34s
2025-10-01 21:56:18 +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
15a2a9c075 overtime only appearing, when there is a booking
All checks were successful
Tests / Run Go Tests (push) Successful in 29s
2025-09-30 00:15:48 +02:00
90193e9346 closes #38, #39, #40
All checks were successful
Tests / Run Go Tests (push) Successful in 37s
2025-09-28 23:29:28 +02:00
tom
e8f1113293 using IWorkDay interface for team
All checks were successful
Tests / Run Go Tests (push) Successful in 42s
2025-09-25 21:52:53 +02:00
tom
db6fc10c28 added interface for workday and absence + multiday absences closes #38, #39 2025-09-23 12:30:02 +02:00
tom
55b0332600 minor fixes 2025-09-16 11:53:41 +02:00
tom
0e1e0b2de0 added volume, to expose logs on host
All checks were successful
Tests / Run Go Tests (push) Successful in 33s
2025-09-15 13:25:33 +02:00
7ceef2c344 Merge pull request 'dev/ui' (#35) from dev/ui into main
All checks were successful
Tests / Run Go Tests (push) Successful in 1m13s
Arbeitszeitmessung Deploy / Run Go Tests (push) Successful in 27s
Arbeitszeitmessung Deploy / Build Go Image and Upload (push) Successful in 1m31s
Reviewed-on: #35
2025-09-15 12:40:26 +02:00
823cb859ea Merge branch 'main' into dev/ui
All checks were successful
Tests / Run Go Tests (push) Successful in 49s
2025-09-15 12:40:00 +02:00
tom
656d4c2340 small fixes in pdf generation + time calculation
All checks were successful
Tests / Run Go Tests (push) Successful in 27s
2025-09-15 12:33:46 +02:00
tom
2d0b117403 added Gleitzeit + Kurzarbeit closes #23
All checks were successful
Tests / Run Go Tests (push) Successful in 33s
2025-09-13 14:12:39 +02:00
tom
ccded6d76b reworked time Calculations 2025-09-13 14:11:26 +02:00
ec69549d13 Merge pull request 'Adding Functions + Finishing CI config' (#34) from dev/ui into main
Some checks failed
Tests / Run Go Tests (push) Failing after 1m15s
Arbeitszeitmessung Deploy / Run Go Tests (push) Successful in 1m17s
Arbeitszeitmessung Deploy / Build Go Image and Upload (push) Successful in 1m44s
Reviewed-on: #34
2025-09-10 09:59:09 +02:00
tom
3d76778d4f Update build.yaml
All checks were successful
Tests / Run Go Tests (push) Successful in 31s
2025-09-10 09:46:06 +02:00
tom
b30686ca06 Update test.yaml
All checks were successful
Tests / Run Go Tests (push) Successful in 34s
2025-09-09 11:15:31 +02:00
tom
2f72eebf22 updated workflows 2025-09-09 11:10:53 +02:00
tom
133e73a55c working on pdf export 2025-09-09 11:07:14 +02:00
2eab598348 working on printable PDF Forms
All checks were successful
Tests / Run Go Tests (push) Successful in 30s
2025-09-08 00:32:29 +02:00
12ed9959cb added helper function, and fixed #28
All checks were successful
Tests / Run Go Tests (push) Successful in 29s
2025-09-05 22:24:42 +02:00
de03c100d4 fixed #28
All checks were successful
Tests / Run Go Tests (push) Successful in 28s
2025-09-05 10:36:26 +02:00
9d70d4db17 updated workflows
All checks were successful
Tests / Run Go Tests (push) Successful in 30s
2025-09-05 00:16:10 +02:00
77 changed files with 5296 additions and 2067 deletions

View File

@@ -1,39 +0,0 @@
name: Arbeitszeitmessung Deploy
run-name: ${{ gitea.actor }} is building and deploying arbeitszeitmesssung
on:
workflow_run:
workflows: ["tests"]
types:
- completed
jobs:
build:
name: Build Go Image and Upload
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
startsWith(github.event.workflow_run.head_branch, 'main') &&
startsWith(github.event.workflow_run.head_commit.ref, 'refs/tags/')
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: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
context: Backend
tags: |
git.letsstein.de/tom/arbeitszeitmessung:latest
git.letsstein.de/tom/arbeitszeitmessung:${{ github.ref_name }}

View File

@@ -1,6 +1,9 @@
name: GoLang Tests
run-name: ${{ gitea.actor }} is testing golang Code
on: push
name: Arbeitszeitmessung Deploy
run-name: ${{ gitea.actor }} is building and deploying arbeitszeitmesssung
on:
push:
tags:
- "*"
jobs:
testing:
@@ -46,3 +49,29 @@ jobs:
arbeitszeitmessung-
- name: Run Go Tests
run: cd Backend && go test ./...
build:
name: Build Go Image and Upload
runs-on: ubuntu-latest
needs: [testing]
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: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
context: Backend
tags: |
git.letsstein.de/tom/arbeitszeitmessung:latest
git.letsstein.de/tom/arbeitszeitmessung:${{ github.ref_name }}

View File

@@ -0,0 +1,71 @@
name: Tests
run-name: ${{ gitea.actor }} is testing golang Code
on: [push]
jobs:
testing:
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_API_USER: root
POSTGRES_API_PASS: password
POSTGRES_DB: arbeitszeitmessung
POSTGRES_PORT: 5432
RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Disabling shallow clone is recommended for improving relevancy of reporting
fetch-depth: 0
- 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 && 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
.scannerwork
Backend/logs
.worktime.txt

View File

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

View File

@@ -14,7 +14,8 @@ func OpenDatabase() (models.IDatabase, error) {
dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung")
dbUser := helper.GetEnv("POSTGRES_API_USER", "api_nutzer")
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)
}

View File

@@ -0,0 +1,229 @@
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.GetTimesVirtual(u, models.WorktimeBaseWeek)
thisTypstDay.Date = day.Date().Format(DE_DATE)
thisTypstDay.Worktime = helper.FormatDurationFill(work, true)
thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true)
thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true)
thisTypstDay.IsFriday = day.Date().Weekday() == time.Friday
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() {
tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user)
typstDayParts = append(typstDayParts, typstDayPart{
BookingFrom: tsFrom.Format("15:04"),
BookingTo: tsTo.Format("15:04"),
WorkType: "Kurzarbeit",
IsWorkDay: true,
})
}
} else {
absentDay, _ := day.(*models.Absence)
typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name})
}
return typstDayParts
}
func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) {
var markup bytes.Buffer
var output bytes.Buffer
if err := typst.InjectValues(&markup, map[string]any{"meta": metadata, "days": days}); 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
#show: doc => abrechnung(meta, days)
`)
// Compile the prepared markup with Typst and write the result it into `output.pdf`.
// f, err := os.Create("output.pdf")
// if err != nil {
// log.Panicf("Failed to create output file: %v.", err)
// }
// defer f.Close()
//
// typstCLI := typst.CLI{}
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
}
func PDFCreateController(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r)
switch r.Method {
case http.MethodGet:
user, err := models.GetUserFromSession(Session, r.Context())
if err != nil {
log.Println("Error getting user!")
return
}
pp := paramParser.New(r.URL.Query())
startDate := pp.ParseTimestampFallback("start_date", time.DateOnly, time.Now())
var members []models.User = make([]models.User, 0)
output, err := createReports(user, members, startDate)
if err != nil {
slog.Warn("Could not create pdf report", slog.Any("Error", err))
}
w.Header().Set("Content-type", "application/pdf")
output.WriteTo(w)
w.WriteHeader(200)
default:
http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed)
}
}
func createReports(user models.User, employes []models.User, startDate time.Time) (bytes.Buffer, error) {
if startDate.Day() > 1 {
startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1))
}
endDate := startDate.AddDate(0, 1, -1)
return createEmployeReport(user, startDate, endDate)
}
func createEmployeReport(employee models.User, startDate, endDate time.Time) (bytes.Buffer, error) {
targetHours := (employee.ArbeitszeitProWoche() / 5) * time.Duration(helper.GetWorkingDays(startDate, endDate))
workingDays := models.GetDays(employee, startDate, endDate, false)
slog.Debug("Baseline Working hours", "targetHours", targetHours.Hours())
var actualHours time.Duration
for _, day := range workingDays {
actualHours += day.TimeWorkVirtual(employee)
}
worktimeBalance := actualHours - targetHours
typstDays, err := convertDaysToTypst(workingDays, employee)
if err != nil {
log.Panicf("Failed to convert days!")
}
metadata := typstMetadata{
EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name),
TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)),
Overtime: helper.FormatDurationFill(worktimeBalance, true),
WorkTime: helper.FormatDurationFill(actualHours, true),
OvertimeTotal: "",
CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"),
}
return renderPDF(typstDays, metadata)
}
func PDFHandler(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r)
startDate := time.Now()
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())
if err != nil {
log.Println("Error getting user!")
}
//TODO: only accepted weeks
weeks := models.GetDays(user, startDate, endDate, false)
var aggregatedOvertime, aggregatedWorkTime time.Duration
for _, day := range weeks {
aggregatedOvertime += day.TimeOvertimeReal(user)
aggregatedWorkTime += day.TimeWorkVirtual(user)
}
typstDays, err := convertDaysToTypst(weeks, user)
if err != nil {
log.Panicf("Failed to convert days!")
}
metadata := typstMetadata{
EmployeeName: fmt.Sprintf("%s %s", user.Vorname, user.Name),
TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)),
Overtime: helper.FormatDurationFill(aggregatedOvertime, true),
WorkTime: helper.FormatDurationFill(aggregatedWorkTime, true),
OvertimeTotal: "",
CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"),
}
output, err := renderPDF(typstDays, metadata)
if err != nil {
slog.Warn("Could not create pdf report", slog.Any("Error", err))
}
w.Header().Set("Content-type", "application/pdf")
output.WriteTo(w)
w.WriteHeader(200)
}
type typstMetadata struct {
TimeRange string `json:"time-range"`
EmployeeName string `json:"employee-name"`
WorkTime string `json:"worktime"`
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"`
IsFriday bool `json:"is-weekend"`
}

25
Backend/endpoints/pdf.go Normal file
View File

@@ -0,0 +1,25 @@
package endpoints
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"arbeitszeitmessung/templates"
"log/slog"
"net/http"
)
func PDFFormHandler(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r)
user, err := models.GetUserFromSession(Session, r.Context())
if err != nil {
slog.Warn("Error getting user!", slog.Any("Error", err))
// TODO add error handling
}
teamMembers, err := user.GetTeamMembers()
if err != nil {
slog.Warn("Error getting team members!", slog.Any("Error", err))
}
templates.PDFForm(teamMembers).Render(r.Context(), w)
}

View File

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

View File

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

View File

@@ -2,13 +2,15 @@ package endpoints
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/paramParser"
"arbeitszeitmessung/models"
"arbeitszeitmessung/templates"
"context"
"database/sql"
"encoding/json"
"log"
"log/slog"
"net/http"
"sort"
"strconv"
"time"
)
@@ -30,12 +32,28 @@ func TimeHandler(w http.ResponseWriter, r *http.Request) {
}
}
func AbsencHandler(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r)
helper.SetCors(w)
switch r.Method {
case http.MethodPost:
err := updateAbsence(r)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/time", 301)
default:
http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed)
}
}
func parseTimestamp(r *http.Request, getKey string, fallback string) (time.Time, error) {
getTimestamp := r.URL.Query().Get(getKey)
if getTimestamp == "" {
getTimestamp = fallback
}
Timestamp, err := time.Parse("2006-01-02", getTimestamp)
Timestamp, err := time.Parse(time.DateOnly, getTimestamp)
if err != nil {
return time.Now(), err
}
@@ -44,40 +62,29 @@ func parseTimestamp(r *http.Request, getKey string, fallback string) (time.Time,
// Returns bookings from DB with similar card uid -> checks for card uid in http query params
func getBookings(w http.ResponseWriter, r *http.Request) {
user, err := (*models.User).GetUserFromSession(nil, Session, r.Context())
user, err := models.GetUserFromSession(Session, r.Context())
if err != nil {
log.Println("No user found with the given personal number!")
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
return
}
pp := paramParser.New(r.URL.Query())
// TODO add config for timeoffset
tsFrom, err := parseTimestamp(r, "time_from", time.Now().AddDate(0, -1, 0).Format("2006-01-02"))
if err != nil {
log.Println("Error parsing 'from' time", err)
http.Error(w, "Timestamp 'from' cannot be parsed!", http.StatusBadRequest)
return
}
tsTo, err := parseTimestamp(r, "time_to", time.Now().Format("2006-01-02"))
if err != nil {
log.Println("Error parsing 'to' time", err)
http.Error(w, "Timestamp 'to' cannot be parsed!", http.StatusBadRequest)
return
}
tsFrom := pp.ParseTimestampFallback("time_from", time.DateOnly, time.Now().AddDate(0, -1, 0))
tsTo := pp.ParseTimestampFallback("time_to", time.DateOnly, time.Now())
tsTo = tsTo.AddDate(0, 0, 1) // so that today is inside
workDays := (*models.WorkDay).GetWorkDays(nil, user.CardUID, tsFrom, tsTo)
sort.Slice(workDays, func(i, j int) bool {
return workDays[i].Day.After(workDays[j].Day)
})
days := models.GetDays(user, tsFrom, tsTo, true)
lastSub := user.GetLastWorkWeekSubmission()
var aggregatedOvertime time.Duration
for _, day := range workDays {
if day.Day.Before(lastSub) {
for _, day := range days {
if day.Date().Before(lastSub) {
continue
}
aggregatedOvertime += day.CalcOvertime(user)
aggregatedOvertime += day.TimeOvertimeReal(user)
}
if reportedOvertime, err := user.GetReportedOvertime(); err == nil {
user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute)
@@ -88,27 +95,30 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Accept") == "application/json" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(workDays)
json.NewEncoder(w).Encode(days)
return
}
ctx := context.WithValue(r.Context(), "user", user)
templates.TimePage(workDays, lastSub).Render(ctx, w)
ctx = context.WithValue(ctx, "days", days)
templates.TimePage([]models.WorkDay{}, lastSub).Render(ctx, w)
}
func updateBooking(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
pp := paramParser.New(r.Form)
var loc *time.Location
loc, err := time.LoadLocation(helper.GetEnv("TZ", "Europe/Berlin"))
if err != nil {
log.Println("Error loading location", err)
loc = time.Local
}
user, err := (*models.User).GetUserFromSession(nil, Session, r.Context())
user, err := models.GetUserFromSession(Session, r.Context())
if err != nil {
log.Println("No user found!", err)
return
}
switch r.FormValue("action") {
case "add":
timestamp, err := time.ParseInLocation("2006-01-02|15:04", r.FormValue("date")+"|"+r.FormValue("timestamp"), loc)
@@ -117,10 +127,9 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
return
}
var check_in_out int
check_in_out, err = strconv.Atoi(r.FormValue("check_in_out"))
check_in_out, err := pp.ParseInt("check_in_out")
if err != nil {
log.Println("Error parsing check_in_out", err)
slog.Warn("Error parsing check_in_out")
return
}
@@ -131,14 +140,14 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
log.Printf("Error inserting booking %v -> %v\n", newBooking, err)
}
case "change":
absenceType, err := strconv.Atoi(r.FormValue("absence"))
if err != nil {
log.Println("Error parsing absence type.", err)
absenceType = 0
}
if absenceType != 0 {
createAbsence(absenceType, user, loc, r)
}
// absenceType, err := strconv.Atoi(r.FormValue("absence"))
// if err != nil {
// log.Println("Error parsing absence type.", err)
// absenceType = 0
// }
// if absenceType != 0 {
// createAbsence(absenceType, user, loc, r)
// }
for index, possibleBooking := range r.PostForm {
if len(index) > 7 && index[:7] == "booking" {
booking_id, err := strconv.Atoi(index[8:])
@@ -156,108 +165,89 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
log.Println("Error parsing time!", err)
continue
}
// log.Println("Parsing time", parsedTime)
booking.UpdateTime(parsedTime)
}
}
default:
log.Println("No action from /time found")
}
getBookings(w, r)
}
func createAbsence(absenceType int, user models.User, loc *time.Location, r *http.Request) {
absenceDate, err := time.ParseInLocation("2006-01-02", r.FormValue("date"), loc)
func updateAbsence(r *http.Request) error {
r.ParseForm()
var loc *time.Location
loc, err := time.LoadLocation(helper.GetEnv("TZ", "Europe/Berlin"))
if err != nil {
log.Println("Cannot get date from input! Skipping absence creation", err)
return
log.Println("Error loading location", err)
loc = time.Local
}
absence, err := models.NewAbsence(user.CardUID, absenceType, absenceDate)
dateFrom, err := time.ParseInLocation(time.DateOnly, r.FormValue("date_from"), loc)
if err != nil {
log.Println("Error creating absence!", err)
return
log.Println("Error parsing date_from input for absence", err)
return err
}
err = absence.Insert()
dateTo, err := time.ParseInLocation(time.DateOnly, r.FormValue("date_to"), loc)
if err != nil {
log.Println("Error inserting absence!", err)
return
log.Println("Error parsing date_to input for absence", err)
return err
}
}
func getBookingsAPI(w http.ResponseWriter, r *http.Request) {
_user_pn := r.URL.Query().Get("personal_nummer")
user_pn, err := strconv.Atoi(_user_pn)
if err != nil {
log.Println("No personal numver found!")
http.Error(w, "No personal number found", http.StatusBadRequest)
return
}
user, err := models.GetUserByPersonalNr(user_pn)
if err != nil {
log.Println("No user found with the given personal number!")
http.Error(w, "No user found", http.StatusNotFound)
return
}
// TODO add config for timeoffset
tsFrom, err := parseTimestamp(r, "time_from", time.Now().AddDate(0, -1, 0).Format("2006-01-02"))
if err != nil {
log.Println("Error parsing 'from' time", err)
http.Error(w, "Timestamp 'from' cannot be parsed!", http.StatusBadRequest)
return
}
tsTo, err := parseTimestamp(r, "time_to", time.Now().Format("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
bookings, err := (*models.Booking).GetBookingsGrouped(nil, user.CardUID, tsFrom, tsTo)
if err != nil {
log.Println("Error getting bookings: ", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(bookings)
}
// Updates a booking form the given json body
func updateBookingAPI(w http.ResponseWriter, r *http.Request) {
_booking_id := r.URL.Query().Get("counter_id")
if _booking_id == "" {
http.Error(w, "Missing bookingID query parameter", http.StatusBadRequest)
return
}
booking_id, err := strconv.Atoi(_booking_id)
if err != nil {
http.Error(w, "Invalid bookingID query parameter", http.StatusBadRequest)
return
}
bookingDB, err := (*models.Booking).GetBookingById(nil, booking_id)
if err != nil {
log.Println("Error getting booking: ", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
var booking models.Booking
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
err = dec.Decode(&booking)
if err != nil {
log.Println("Error parsing booking: ", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if booking.CounterId != 0 && booking.CounterId != bookingDB.CounterId {
log.Println("Booking Ids do not match")
http.Error(w, "Booking Ids do not match", http.StatusBadRequest)
return
}
bookingDB.Update(booking)
bookingDB.Save()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(bookingDB)
absenceTypeId, err := strconv.Atoi(r.FormValue("aw_type"))
if err != nil {
log.Println("Error parsing aw_type", err)
return err
}
absenceId, err := strconv.Atoi(r.FormValue("aw_id"))
if err != nil && r.FormValue("aw_id") == "" {
absenceId = 0
} else if err != nil {
log.Println("Error parsing aw_id", err)
return err
}
absenceType, err := models.GetAbsenceTypeById(int8(absenceTypeId))
if err != nil {
log.Println("No matching absence type found!")
return err
}
newAbsence := models.Absence{DateFrom: dateFrom, DateTo: dateTo, AbwesenheitTyp: absenceType}
absence, err := models.GetAbsenceById(absenceId)
if err == sql.ErrNoRows {
err = nil
log.Println("Absence not found creating new!")
user, err := models.GetUserFromSession(Session, r.Context())
if err != nil {
log.Println("No user found!", err)
return err
}
newAbsence.CardUID = user.CardUID
newAbsence.Insert()
}
if err != nil {
log.Println("Cannot get Absence for id: ", absenceId, err)
return err
}
if r.FormValue("action") == "delete" {
log.Println("Deleting Absence!", "Not implemented")
// TODO
//absence.Delete()
return nil
}
if absence.Update(newAbsence) {
err = absence.Save()
if err != nil {
log.Println("Error saving updated absence!", err)
return err
}
}
return nil
}

View File

@@ -3,6 +3,7 @@ package endpoints
import (
"arbeitszeitmessung/models"
"arbeitszeitmessung/templates"
"context"
"log"
"net/http"
)
@@ -38,5 +39,9 @@ func changePassword(w http.ResponseWriter, r *http.Request) {
}
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) {
helper.RequiresLogin(Session, w, r)
switch r.Method {
case http.MethodGet:
showUserPage(w, r, 0)

View File

@@ -1,6 +1,6 @@
module arbeitszeitmessung
go 1.24.5
go 1.24.7
require github.com/lib/pq v1.10.9
@@ -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/Dadido3/go-typst v0.8.0
github.com/golang-migrate/migrate/v4 v4.18.3
github.com/joho/godotenv v1.5.1
)
@@ -16,5 +17,14 @@ require (
require (
github.com/hashicorp/errwrap v1.1.0 // 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
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/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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

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

View File

@@ -0,0 +1,93 @@
package paramParser
import (
"fmt"
"log/slog"
"net/url"
"strconv"
"time"
)
type ParamsParser struct {
urlParams url.Values
}
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, fallback string) (string, error) {
if !p.urlParams.Has(key) {
return "", &NoValueError{Key: key}
}
return p.urlParams.Get(key), nil
}
func (p *ParamsParser) ParseIntFallback(key string, fallback int) int {
if !p.urlParams.Has(key) {
return fallback
}
paramInt := p.urlParams.Get(key)
if result, err := strconv.Atoi(paramInt); err == nil {
return result
} else {
slog.Warn("Error parsing HTTP Params to Int", slog.Any("key", key), slog.Any("error", err))
return fallback
}
}
func (p *ParamsParser) ParseInt(key string) (int, error) {
if !p.urlParams.Has(key) {
return 0, &NoValueError{Key: key}
}
paramInt := p.urlParams.Get(key)
if result, err := strconv.Atoi(paramInt); err == nil {
return result, nil
} else {
slog.Debug("Error parsing HTTP Params to Int", slog.Any("key", key), slog.Any("error", err))
return 0, err
}
}

View File

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

View File

@@ -20,8 +20,17 @@ func IsWeekend(ts time.Time) bool {
return ts.Weekday() == time.Saturday || ts.Weekday() == time.Sunday
}
// Converts duration to string
func GetKW(t time.Time) int {
_, kw := t.ISOWeek()
return kw
}
func FormatDuration(d time.Duration) string {
return FormatDurationFill(d, false)
}
// Converts duration to string
func FormatDurationFill(d time.Duration, fill bool) string {
hours := int(d.Abs().Hours())
minutes := int(d.Abs().Minutes()) % 60
sign := ""
@@ -36,6 +45,33 @@ func FormatDuration(d time.Duration) string {
case minutes > 0:
return fmt.Sprintf("%s%dmin", sign, minutes)
default:
if fill {
return "0min"
}
return ""
}
}
func IsSameDate(a, b time.Time) bool {
return a.Truncate(24 * time.Hour).Equal(b.Truncate(24 * time.Hour))
}
func GetWorkingDays(startDate, endDate time.Time) int {
if endDate.Before(startDate) {
return 0
}
var count int = 0
for d := startDate.Truncate(24 * time.Hour); !d.After(endDate); d = d.Add(24 * time.Hour) {
if !IsWeekend(d) {
count++
}
}
return count
}
func FormatGermanDayOfWeek(t time.Time) string {
return days[t.Weekday()][:2]
}
var days = [...]string{
"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"}

View File

@@ -1,23 +1,33 @@
package helper
import (
"fmt"
"testing"
"time"
)
func TestGetMonday(t *testing.T) {
isMonday, err := time.Parse("2006-01-02", "2025-07-14")
notMonday, err := time.Parse("2006-01-02", "2025-07-16")
isMonday, err := time.Parse(time.DateOnly, "2025-07-14")
isSunday, err := time.Parse(time.DateOnly, "2025-07-20")
notMonday, err := time.Parse(time.DateOnly, "2025-07-16")
if err != nil || isMonday.Equal(notMonday) {
t.Errorf("U stupid? %e", err)
}
if GetMonday(isMonday) != isMonday || GetMonday(notMonday) != isMonday {
if GetMonday(isMonday) != isMonday {
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) {
durations := []struct {
testCases := []struct {
name string
duration time.Duration
}{
@@ -25,13 +35,82 @@ func TestFormatDuration(t *testing.T) {
{"30min", time.Duration(30 * time.Minute)},
{"1h 30min", time.Duration(90 * time.Minute)},
{"-1h 30min", time.Duration(-90 * time.Minute)},
{"", 0},
{"0min", 0},
}
for _, d := range durations {
t.Run(d.name, func(t *testing.T) {
if FormatDuration(d.duration) != d.name {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if FormatDurationFill(tc.duration, true) != 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!")
}
})
}
}

View File

@@ -3,7 +3,23 @@ package helper
import "time"
type TimeFormValue struct {
TsFrom time.Time
TsTo time.Time
TsFrom time.Time
TsTo time.Time
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

@@ -5,8 +5,7 @@ import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"context"
"fmt"
"log"
"log/slog"
"net/http"
"os"
"time"
@@ -17,23 +16,25 @@ import (
func main() {
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")
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" {
log.Println("Debug mode enabled")
log.Println("Environment Variables")
logLevel.Set(slog.LevelDebug)
envs := os.Environ()
for _, e := range envs {
fmt.Println(e)
}
slog.Debug("Debug mode enabled", "Environment Variables", envs)
}
models.DB, err = OpenDatabase()
if err != nil {
log.Fatal(err)
slog.Error("Error while opening the database", "Error", err)
}
fs := http.FileServer(http.Dir("./static"))
@@ -43,27 +44,31 @@ func main() {
// handles the different http routes
server.HandleFunc("/time/new", endpoints.TimeCreateHandler)
server.Handle("/absence", ParamsMiddleware(endpoints.AbsencHandler))
server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler))
server.HandleFunc("/logout", endpoints.LogoutHandler)
server.HandleFunc("/user/{action}", endpoints.UserHandler)
// server.HandleFunc("/user/login", endpoints.LoginHandler)
// server.HandleFunc("/user/settings", endpoints.UserSettingsHandler)
server.HandleFunc("/team", endpoints.TeamHandler)
server.HandleFunc("/team/presence", endpoints.TeamPresenceHandler)
server.HandleFunc("/presence", endpoints.TeamPresenceHandler)
server.Handle("/pdf", ParamsMiddleware(endpoints.PDFFormHandler))
server.HandleFunc("/pdf/generate", endpoints.PDFCreateController)
server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect))
server.Handle("/static/", http.StripPrefix("/static/", fs))
serverSessionMiddleware := endpoints.Session.LoadAndSave(server)
// starting the http server
fmt.Printf("Server is running at http://localhost:%s\n", helper.GetEnv("EXPOSED_PORT", "8080"))
log.Fatal(http.ListenAndServe(":8080", serverSessionMiddleware))
slog.Info("Server is running at http://localhost:8080")
slog.Error("Error starting Server", "Error", http.ListenAndServe(":8080", serverSessionMiddleware))
}
func ParamsMiddleware(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query()
ctx := context.WithValue(r.Context(), "urlParams", queryParams)
slog.Debug("ParamsMiddleware added urlParams", slog.Any("urlParams", queryParams))
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -1,23 +1,25 @@
package models
import (
"encoding/json"
"errors"
"log"
"time"
)
type AbsenceType struct {
Id int8
Name string
WorkTime float32
Id int8 `json:"abwesenheit_id"`
Name string `json:"abwesenheit_name"`
WorkTime int8 `json:"arbeitszeit_equivalent"`
}
type Absence struct {
Day time.Time
CounterId int
CardUID string
AbwesenheitTyp AbsenceType
Datum time.Time
// Comment string
DateFrom time.Time
DateTo time.Time
}
func NewAbsence(card_uid string, abwesenheit_typ int, datum time.Time) (Absence, error) {
@@ -25,7 +27,7 @@ func NewAbsence(card_uid string, abwesenheit_typ int, datum time.Time) (Absence,
return Absence{
CardUID: card_uid,
AbwesenheitTyp: AbsenceType{0, "Custom absence", 100},
Datum: datum,
DateFrom: datum,
}, nil
}
_absenceType, ok := GetAbsenceTypesCached()[int8(abwesenheit_typ)]
@@ -35,18 +37,124 @@ func NewAbsence(card_uid string, abwesenheit_typ int, datum time.Time) (Absence,
return Absence{
CardUID: card_uid,
AbwesenheitTyp: _absenceType,
Datum: datum,
DateFrom: datum,
}, nil
}
func (a *Absence) Date() time.Time {
return a.Day.Truncate(24 * time.Hour)
}
func (a *Absence) IsMultiDay() bool {
return !a.DateFrom.Equal(a.DateTo)
}
func (a *Absence) GetWorktimeReal(u User, base WorktimeBase) time.Duration {
if a.AbwesenheitTyp.WorkTime <= 1 {
return 0
}
switch base {
case WorktimeBaseDay:
return u.ArbeitszeitProTag()
case WorktimeBaseWeek:
return u.ArbeitszeitProWoche() / 5
}
return 0
}
func (a *Absence) GetPausetimeReal(u User, base WorktimeBase) time.Duration {
return 0
}
func (a *Absence) GetOvertimeReal(u User, base WorktimeBase) time.Duration {
if a.AbwesenheitTyp.WorkTime > 1 {
return 0
}
switch base {
case WorktimeBaseDay:
return -u.ArbeitszeitProTag()
case WorktimeBaseWeek:
return -u.ArbeitszeitProWoche() / 5
}
return 0
}
func (a *Absence) GetWorktimeVirtual(u User, base WorktimeBase) time.Duration {
return a.GetWorktimeReal(u, base)
}
func (a *Absence) GetPausetimeVirtual(u User, base WorktimeBase) time.Duration {
return a.GetPausetimeReal(u, base)
}
func (a *Absence) GetOvertimeVirtual(u User, base WorktimeBase) time.Duration {
return a.GetOvertimeReal(u, base)
}
func (a *Absence) GetTimesReal(u User, base WorktimeBase) (work, pause, overtime time.Duration) {
return a.GetWorktimeReal(u, base), a.GetPausetimeReal(u, base), a.GetOvertimeReal(u, base)
}
func (a *Absence) GetTimesVirtual(u User, base WorktimeBase) (work, pause, overtime time.Duration) {
return a.GetWorktimeVirtual(u, base), a.GetPausetimeVirtual(u, base), a.GetOvertimeVirtual(u, base)
}
func (a *Absence) TimeWorkVirtual(u User) time.Duration {
return a.TimeWorkReal(u)
}
func (a *Absence) TimeWorkReal(u User) time.Duration {
if a.AbwesenheitTyp.WorkTime > 1 {
return time.Duration(u.ArbeitszeitPerTag * float32(time.Hour)).Round(time.Minute)
}
return 0
}
func (a *Absence) TimePauseReal(u User) (work, pause time.Duration) {
return 0, 0
}
func (a *Absence) TimeOvertimeReal(u User) time.Duration {
if a.AbwesenheitTyp.WorkTime > 1 {
return 0
}
return -u.ArbeitszeitProTag()
}
func (a *Absence) ToString() string {
return "Abwesenheit"
}
func (a *Absence) IsWorkDay() bool {
return false
}
func (a *Absence) IsKurzArbeit() bool {
return false
}
func (a *Absence) GetDayProgress(u User) int8 {
return 100
}
func (a *Absence) RequiresAction() bool {
return false
}
func (a *Absence) GetAllWorkTimesVirtual(u User) (work, pause, overtime time.Duration) {
if a.AbwesenheitTyp.WorkTime > 1 {
return u.ArbeitszeitProTag(), 0, 0
}
return 0, 0, 0
}
func (a *Absence) Insert() error {
qStr, err := DB.Prepare(`INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum) VALUES ($1, $2, $3) RETURNING counter_id;`)
qStr, err := DB.Prepare(`INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum_from, datum_to) VALUES ($1, $2, $3, $4) RETURNING counter_id;`)
if err != nil {
log.Println("Error preparing sql Statement", err)
return err
}
defer qStr.Close()
err = qStr.QueryRow(a.CardUID, a.AbwesenheitTyp.Id, a.Datum).Scan(&a.CounterId)
err = qStr.QueryRow(a.CardUID, a.AbwesenheitTyp.Id, a.DateFrom, a.DateTo).Scan(&a.CounterId)
if err != nil {
log.Println("Error executing insert statement", err)
return err
@@ -54,14 +162,31 @@ func (a *Absence) Insert() error {
return nil
}
func (a *Absence) Save() error {
qStr, err := DB.Prepare(`
UPDATE abwesenheit SET card_uid = $2, abwesenheit_typ = $3, datum_from = $4, datum_to = $5 WHERE counter_id = $1;
`)
if err != nil {
log.Println("Error preparing sql Statement (Absence Save)", err)
return err
}
defer qStr.Close()
_, err = qStr.Query(a.CounterId, a.CardUID, a.AbwesenheitTyp.Id, a.DateFrom, a.DateTo)
if err != nil {
log.Println("Error executing update statement", err)
return err
}
return nil
}
func GetAbsenceById(counterId int) (Absence, error) {
var absence Absence = Absence{CounterId: counterId}
qStr, err := DB.Prepare("SELECT card_uid, abwesenheit_typ, datum FROM abwesenheit WHERE counter_id = $1;")
qStr, err := DB.Prepare("SELECT card_uid, abwesenheit_typ, datum_from, datum_to FROM abwesenheit WHERE counter_id = $1;")
if err != nil {
return absence, err
}
defer qStr.Close()
err = qStr.QueryRow(counterId).Scan(&absence.CardUID, &absence.AbwesenheitTyp.Id, &absence.Datum)
err = qStr.QueryRow(counterId).Scan(&absence.CardUID, &absence.AbwesenheitTyp.Id, &absence.DateFrom, &absence.DateTo)
if err != nil {
return absence, err
}
@@ -70,7 +195,32 @@ func GetAbsenceById(counterId int) (Absence, error) {
func GetAbsencesByCardUID(card_uid string, tsFrom time.Time, tsTo time.Time) ([]Absence, error) {
var absences []Absence
qStr, err := DB.Prepare("SELECT counter_id, abwesenheit_typ, datum FROM abwesenheit WHERE card_uid = $1 AND datum BETWEEN $2::DATE AND $3::DATE ORDER BY datum;")
// qStr, err := DB.Prepare(`SELECT counter_id, abwesenheit_typ, datum_from, datum_to FROM abwesenheit WHERE card_uid = $1 AND datum_from <= $2 AND datum_to >= $3 ORDER BY datum_from;`)
qStr, err := DB.Prepare(`
SELECT
ab.counter_id,
gs::DATE AS work_date,
ab.card_uid,
ab.datum_from,
ab.datum_to,
jsonb_build_object(
'abwesenheit_id', sat.abwesenheit_id,
'abwesenheit_name', sat.abwesenheit_name,
'arbeitszeit_equivalent', sat.arbeitszeit_equivalent
) AS abwesenheit_info
FROM generate_series(
$2,
$3,
INTERVAL '1 day'
) gs
JOIN abwesenheit ab
ON ab.card_uid = $1
AND ab.datum_from::DATE <= gs::DATE
AND ab.datum_to::DATE >= gs::DATE
LEFT JOIN s_abwesenheit_typen sat
ON ab.abwesenheit_typ = sat.abwesenheit_id
ORDER BY gs::DATE, ab.counter_id;
`)
if err != nil {
return absences, err
}
@@ -82,15 +232,16 @@ func GetAbsencesByCardUID(card_uid string, tsFrom time.Time, tsTo time.Time) ([]
defer rows.Close()
for rows.Next() {
var absence Absence
if err := rows.Scan(&absence.CounterId, &absence.AbwesenheitTyp.Id, &absence.Datum); err != nil {
var abwesenheitsTyp []byte
if err := rows.Scan(&absence.CounterId, &absence.Day, &absence.CardUID, &absence.DateFrom, &absence.DateTo, &abwesenheitsTyp); err != nil {
return absences, err
}
absence.AbwesenheitTyp, err = GetAbsenceTypeById(absence.AbwesenheitTyp.Id)
if err == nil {
absences = append(absences, absence)
} else {
log.Println("Cannot populate absence type!", err)
err = json.Unmarshal(abwesenheitsTyp, &absence.AbwesenheitTyp)
if err != nil {
log.Println("Error parsing abwesenheitsTyp to JSON!", err)
return absences, nil
}
absences = append(absences, absence)
}
if err = rows.Err(); err != nil {
return absences, err
@@ -98,6 +249,27 @@ func GetAbsencesByCardUID(card_uid string, tsFrom time.Time, tsTo time.Time) ([]
return absences, nil
}
func (a *Absence) Update(na Absence) bool {
change := false
if a.CardUID != na.CardUID && na.CardUID != "" {
a.CardUID = na.CardUID
change = true
}
if a.AbwesenheitTyp != na.AbwesenheitTyp && na.AbwesenheitTyp.Id != 0 {
a.AbwesenheitTyp = na.AbwesenheitTyp
change = true
}
if !a.DateFrom.Equal(na.DateFrom) && !na.DateFrom.IsZero() {
a.DateFrom = na.DateFrom
change = true
}
if !a.DateTo.Equal(na.DateTo) && !na.DateTo.IsZero() {
a.DateTo = na.DateTo
change = true
}
return change
}
func GetAbsenceTypes() (map[int8]AbsenceType, error) {
var types = make(map[int8]AbsenceType)
qStr, err := DB.Prepare("SELECT abwesenheit_id, abwesenheit_name, arbeitszeit_equivalent FROM s_abwesenheit_typen;")

View File

@@ -6,8 +6,8 @@ import (
"database/sql"
"fmt"
"log"
"log/slog"
"net/url"
"sort"
"strconv"
"time"
)
@@ -15,8 +15,8 @@ import (
type SameBookingError struct{}
type BookingType struct {
Id int8
Name string
Id int8 `json:"anwesenheit_id"`
Name string `json:"anwesenheit_name"`
}
func (e SameBookingError) Error() string {
@@ -29,7 +29,7 @@ type Booking struct {
CheckInOut int16 `json:"check_in_out"`
Timestamp time.Time `json:"timestamp"`
CounterId int `json:"counter_id"`
BookingType BookingType `json:"booking_type"`
BookingType BookingType `json:"anwesenheit_typ"`
}
type IDatabase interface {
@@ -88,6 +88,27 @@ func (b *Booking) Verify() bool {
return true
}
func (b *Booking) IsSubmittedAndChecked() bool {
qStr, err := DB.Prepare(`SELECT bestaetigt from wochen_report WHERE $1 = ANY(anwesenheiten);`)
if err != nil {
slog.Warn("Error when preparing SQL Statement", "error", err)
return false
}
defer qStr.Close()
var isSubmittedAndChecked bool = false
err = qStr.QueryRow(b.CounterId).Scan(&isSubmittedAndChecked)
if err == sql.ErrNoRows {
// No rows found ==> not even submitted
return false
}
if err != nil {
slog.Warn("Unexpected error when executing SQL Statement", "error", err)
}
return isSubmittedAndChecked
}
func (b *Booking) Insert() error {
if !checkLastBooking(*b) {
return SameBookingError{}
@@ -163,37 +184,6 @@ func (b *Booking) GetBookingsByCardID(card_uid string, tsFrom time.Time, tsTo ti
return bookings, nil
}
func (b *Booking) GetBookingsGrouped(card_uid string, tsFrom time.Time, tsTo time.Time) ([]WorkDay, error) {
var grouped = make(map[string][]Booking)
bookings, err := b.GetBookingsByCardID(card_uid, tsFrom, tsTo)
if err != nil {
log.Println("Failed to get bookings", err)
return []WorkDay{}, nil
}
for _, booking := range bookings {
day := booking.Timestamp.Truncate(24 * time.Hour)
key := day.Format("2006-01-02")
grouped[key] = append(grouped[key], booking)
}
var result []WorkDay
for key, bookings := range grouped {
day, _ := time.Parse("2006-01-02", key)
sort.Slice(bookings, func(i, j int) bool {
return bookings[i].Timestamp.Before(bookings[j].Timestamp)
})
workDay := WorkDay{Day: day, Bookings: bookings}
workDay.getWorkTime()
result = append(result, workDay)
}
sort.Slice(result, func(i, j int) bool {
return result[i].Day.After(result[j].Day)
})
return result, nil
}
func (b Booking) Save() {
qStr, err := DB.Prepare((`UPDATE "anwesenheit" SET "card_uid" = $2, "geraet_id" = $3, "check_in_out" = $4, "timestamp" = $5 WHERE "counter_id" = $1;`))
if err != nil {
@@ -298,7 +288,7 @@ func (b *Booking) UpdateTime(newTime time.Time) {
}
func (b *Booking) ToString() string {
return fmt.Sprintf("Booking %d: at: %s, as type: %d", b.CounterId, b.Timestamp.Format("15:04"), b.CheckInOut)
return fmt.Sprintf("Booking %d: at: %s, CheckInOut: %d, TypeId: %d", b.CounterId, b.Timestamp.Format("15:04"), b.CheckInOut, b.BookingType.Id)
}
func GetBookingTypes() ([]BookingType, error) {

View File

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

View File

@@ -22,6 +22,9 @@ type DBFixture struct {
func SetupDBFixture(t *testing.T) *DBFixture {
t.Helper()
if helper.GetEnv("TEST_SQL", "false") != "true" {
t.Skip("Skipping Test because TEST_SQL is not 'true'!")
}
dbHost := helper.GetEnv("POSTGRES_HOST", "localhost")
dbPort := helper.GetEnv("POSTGRES_PORT", "5433")

View File

@@ -22,7 +22,7 @@ type User struct {
Overtime time.Duration
}
func (u *User) GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) {
func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) {
var user User
var err error
if helper.GetEnv("GO_ENV", "production") == "debug" {
@@ -85,6 +85,15 @@ func (u *User) GetAll() ([]User, error) {
return users, nil
}
// Returns the worktime per day rounded to minutes
func (u *User) ArbeitszeitProTag() time.Duration {
return time.Duration(u.ArbeitszeitPerTag * float32(time.Hour)).Round(time.Minute)
}
func (u *User) ArbeitszeitProWoche() time.Duration {
return time.Duration(u.ArbeitszeitPerWoche * float32(time.Hour)).Round(time.Minute)
}
// Returns true if there is a booking 1 for today -> meaning the user is at work
// Returns false if there is no booking today or the user is already booked out of the system
func (u *User) CheckAnwesenheit() bool {
@@ -164,7 +173,7 @@ func (u *User) ChangePass(password, newPassword string) (bool, error) {
func (u *User) GetTeamMembers() ([]User, error) {
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 {
return teamMembers, err
}
@@ -198,17 +207,6 @@ func (u *User) IsTeamLeader() bool {
return len(team) > 0
}
func (u *User) GetWeek(tsFrom time.Time) WorkWeek {
var bookings []WorkDay
weekStart := tsFrom.AddDate(0, 0, -1*int(tsFrom.Local().Weekday())-1)
bookings, err := (*Booking).GetBookingsGrouped(nil, u.CardUID, weekStart, time.Now())
if err != nil {
log.Println("Error fetching bookings!")
return WorkWeek{WorkDays: bookings}
}
return WorkWeek{WorkDays: bookings}
}
// gets the first week, that needs to be submitted
func (u *User) GetNextWeek() WorkWeek {
var week WorkWeek

View File

@@ -2,86 +2,360 @@ package models
import (
"arbeitszeitmessung/helper"
"database/sql"
"encoding/json"
"fmt"
"log"
"strconv"
"log/slog"
"sort"
"time"
)
type WorkDay struct {
Day time.Time `json:"day"`
Bookings []Booking `json:"bookings"`
workTime time.Duration
pauseTime time.Duration
TimeFrom time.Time
TimeTo time.Time
Absence Absence
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
GetWorktimeReal(User, WorktimeBase) time.Duration
GetPausetimeReal(User, WorktimeBase) time.Duration
GetOvertimeReal(User, WorktimeBase) time.Duration
GetWorktimeVirtual(User, WorktimeBase) time.Duration
GetPausetimeVirtual(User, WorktimeBase) time.Duration
GetOvertimeVirtual(User, WorktimeBase) time.Duration
GetTimesReal(User, WorktimeBase) (work, pause, overtime time.Duration)
GetTimesVirtual(User, WorktimeBase) (work, pause, overtime time.Duration)
}
func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay {
type WorkDay struct {
Day time.Time `json:"day"`
Bookings []Booking `json:"bookings"`
workTime time.Duration
pauseTime time.Duration
realWorkTime time.Duration
realPauseTime time.Duration
TimeFrom time.Time
TimeTo time.Time
kurzArbeit bool
kurzArbeitAbsence Absence
}
type WorktimeBase string
const (
WorktimeBaseWeek WorktimeBase = "week"
WorktimeBaseDay WorktimeBase = "day"
)
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
}
if day.AbwesenheitTyp.WorkTime == 1 {
if workDay, ok := allDays[day.Date().Format(time.DateOnly)].(*WorkDay); ok && len(workDay.Bookings) > 0 {
workDay.kurzArbeit = true
workDay.kurzArbeitAbsence = day
}
} else {
allDays[day.Date().Format(time.DateOnly)] = &day
}
}
sortedDays := sortDays(allDays, orderedForward)
return sortedDays
}
// Gets the time as is in the db (with corrected pause times)
func (d *WorkDay) GetWorktimeReal(u User, base WorktimeBase) time.Duration {
work, pause := calcWorkPause(d.Bookings)
work, pause = correctWorkPause(work, pause)
return work
}
// Gets the corrected pause times based on db entries
func (d *WorkDay) GetPausetimeReal(u User, base WorktimeBase) time.Duration {
work, pause := calcWorkPause(d.Bookings)
work, pause = correctWorkPause(work, pause)
return pause
}
// Returns the overtime based on the db entries
func (d *WorkDay) GetOvertimeReal(u User, base WorktimeBase) time.Duration {
work, pause := calcWorkPause(d.Bookings)
work, pause = correctWorkPause(work, pause)
var targetHours time.Duration
switch base {
case WorktimeBaseDay:
targetHours = u.ArbeitszeitProTag()
case WorktimeBaseWeek:
targetHours = u.ArbeitszeitProWoche() / 5
}
return work - targetHours
}
// Returns the worktime based on absence or kurzarbeit
func (d *WorkDay) GetWorktimeVirtual(u User, base WorktimeBase) time.Duration {
if !d.IsKurzArbeit() {
return d.GetWorktimeReal(u, base)
}
switch base {
case WorktimeBaseDay:
return u.ArbeitszeitProTag()
case WorktimeBaseWeek:
return u.ArbeitszeitProWoche() / 5
}
return 0
}
func (d *WorkDay) GetPausetimeVirtual(u User, base WorktimeBase) time.Duration {
return d.GetPausetimeReal(u, base)
}
func (d *WorkDay) GetOvertimeVirtual(u User, base WorktimeBase) time.Duration {
work := d.GetWorktimeVirtual(u, base)
var targetHours time.Duration
switch base {
case WorktimeBaseDay:
targetHours = u.ArbeitszeitProTag()
case WorktimeBaseWeek:
targetHours = u.ArbeitszeitProWoche() / 5
}
return work - targetHours
}
func (d *WorkDay) GetTimesReal(u User, base WorktimeBase) (work, pause, overtime time.Duration) {
return d.GetWorktimeReal(u, base), d.GetPausetimeReal(u, base), d.GetOvertimeReal(u, base)
}
func (d *WorkDay) GetTimesVirtual(u User, base WorktimeBase) (work, pause, overtime time.Duration) {
return d.GetWorktimeVirtual(u, base), d.GetPausetimeVirtual(u, base), d.GetOvertimeVirtual(u, base)
}
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 {
work += b.Timestamp.Sub(lastBooking.Timestamp)
}
lastBooking = b
}
if len(bookings)%2 == 1 {
work += time.Since(lastBooking.Timestamp.Local())
}
return work, pause
}
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)
}
if forward {
sort.Slice(sortedDays, func(i, j int) bool {
return sortedDays[i].Date().After(sortedDays[j].Date())
})
} else {
sort.Slice(sortedDays, func(i, j int) bool {
return sortedDays[i].Date().Before(sortedDays[j].Date())
})
}
return sortedDays
}
func (d *WorkDay) Date() time.Time {
return d.Day
}
func (d *WorkDay) GenerateKurzArbeitBookings(u User) (time.Time, time.Time) {
var timeFrom, timeTo time.Time
if d.workTime >= u.ArbeitszeitProTag() {
return timeFrom, timeTo
}
timeFrom = d.Bookings[len(d.Bookings)-1].Timestamp.Add(time.Minute)
timeTo = timeFrom.Add(u.ArbeitszeitProTag() - d.workTime)
slog.Debug("Added duration as Kurzarbeit", "date", d.Date().String(), "duration", timeTo.Sub(timeFrom).String())
return timeFrom, timeTo
}
func (d *WorkDay) TimeWorkVirtual(u User) time.Duration {
if d.IsKurzArbeit() {
return u.ArbeitszeitProTag()
}
return d.workTime
}
func (d *WorkDay) GetKurzArbeit() *Absence {
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())
}
// slog.Debug("Calculated RealWorkTime for user", "user", u, slog.String("worktime", d.realWorkTime.String()))
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 {
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 {
return true
}
func (d *WorkDay) SetKurzArbeit(kurzArbeit bool) {
d.kurzArbeit = kurzArbeit
}
func (d *WorkDay) IsKurzArbeit() bool {
return d.kurzArbeit
}
func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
var workDays []WorkDay
var workSec, pauseSec float64
qStr, err := DB.Prepare(`
WITH all_days AS (
SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date
),
SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date),
ordered_bookings AS (
SELECT
timestamp::DATE AS work_date,
timestamp,
check_in_out,
counter_id,
LAG(timestamp) OVER (PARTITION BY card_uid, timestamp::DATE ORDER BY timestamp) AS prev_timestamp,
LAG(check_in_out) OVER (PARTITION BY card_uid, timestamp::DATE ORDER BY timestamp) AS prev_check
FROM anwesenheit
WHERE card_uid = $1
AND timestamp::DATE >= $2
AND timestamp::DATE <= $3
),
abwesenheiten AS (
SELECT
datum::DATE AS work_date,
abwesenheit_typ
FROM abwesenheit
WHERE card_uid = $1
AND datum::DATE >= $2
AND datum::DATE <= $3
)
SELECT
d.work_date,
COALESCE(MIN(b.timestamp), NOW()) AS time_from,
COALESCE(MAX(b.timestamp), NOW()) AS time_to,
COALESCE(
EXTRACT(EPOCH FROM SUM(
CASE
WHEN b.prev_check IN (1, 3) AND b.check_in_out IN (2, 4, 255)
THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0'
END
)), 0
) AS total_work_seconds,
COALESCE(
EXTRACT(EPOCH FROM SUM(
CASE
WHEN b.prev_check IN (2, 4, 255) AND b.check_in_out IN (1, 3)
THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0'
END
)), 0
) AS total_pause_seconds,
COALESCE(jsonb_agg(jsonb_build_object(
'check_in_out', b.check_in_out,
'timestamp', b.timestamp,
'counter_id', b.counter_id
) ORDER BY b.timestamp), '[]'::jsonb) AS bookings,
a.abwesenheit_typ
FROM all_days d
LEFT JOIN ordered_bookings b ON d.work_date = b.work_date
LEFT JOIN abwesenheiten a ON d.work_date = a.work_date
GROUP BY d.work_date, a.abwesenheit_typ
ORDER BY d.work_date ASC;`)
SELECT
a.timestamp::DATE AS work_date,
a.timestamp,
a.check_in_out,
a.counter_id,
a.anwesenheit_typ,
sat.anwesenheit_name AS anwesenheit_typ_name,
LAG(a.timestamp) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_timestamp,
LAG(a.check_in_out) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_check
FROM anwesenheit a
LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id
WHERE a.card_uid = $1
AND a.timestamp::DATE >= $2
AND a.timestamp::DATE <= $3
)
SELECT
d.work_date,
COALESCE(MIN(b.timestamp), NOW()) AS time_from,
COALESCE(MAX(b.timestamp), NOW()) AS time_to,
COALESCE(
EXTRACT(EPOCH FROM SUM(
CASE
WHEN b.prev_check IN (1, 3) AND b.check_in_out IN (2, 4, 254)
THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0'
END
)), 0
) AS total_work_seconds,
COALESCE(
EXTRACT(EPOCH FROM SUM(
CASE
WHEN b.prev_check IN (2, 4, 254) AND b.check_in_out IN (1, 3)
THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0'
END
)), 0
) AS total_pause_seconds,
COALESCE(jsonb_agg(jsonb_build_object(
'check_in_out', b.check_in_out,
'timestamp', b.timestamp,
'counter_id', b.counter_id,
'anwesenheit_typ', b.anwesenheit_typ,
'anwesenheit_typ', jsonb_build_object(
'anwesenheit_id', b.anwesenheit_typ,
'anwesenheit_name', b.anwesenheit_typ_name
)
) ORDER BY b.timestamp), '[]'::jsonb) AS bookings
FROM all_days d
LEFT JOIN ordered_bookings b ON d.work_date = b.work_date
GROUP BY d.work_date
ORDER BY d.work_date ASC;`)
if err != nil {
log.Println("Error preparing SQL statement", err)
@@ -89,18 +363,17 @@ func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay
}
defer qStr.Close()
rows, err := qStr.Query(card_uid, tsFrom, tsTo)
rows, err := qStr.Query(user.CardUID, tsFrom, tsTo)
if err != nil {
log.Println("Error getting rows!")
return workDays
}
defer rows.Close()
emptyDays, _ := strconv.ParseBool(helper.GetEnv("EMPTY_DAYS", "false"))
// emptyDays, _ := strconv.ParseBool(helper.GetEnv("EMPTY_DAYS", "false"))
for rows.Next() {
var workDay WorkDay
var bookings []byte
var absenceType sql.NullInt16
if err := rows.Scan(&workDay.Day, &workDay.TimeFrom, &workDay.TimeTo, &workSec, &pauseSec, &bookings, &absenceType); err != nil {
if err := rows.Scan(&workDay.Day, &workDay.TimeFrom, &workDay.TimeTo, &workSec, &pauseSec, &bookings); err != nil {
log.Println("Error scanning row!", err)
return workDays
}
@@ -115,103 +388,55 @@ func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay
if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 {
workDay.Bookings = []Booking{}
}
if absenceType.Valid {
workDay.Absence, err = NewAbsence(card_uid, int(absenceType.Int16), workDay.Day)
// log.Println("Found absence", workDay.Absence)
}
if workDay.Day.Equal(time.Now().Truncate(24 * time.Hour)) {
workDay.getWorkTime()
} else {
workDay.calcPauseTime()
}
if emptyDays && workDay.Day.Weekday() >= 1 && workDay.Day.Weekday() <= 5 {
workDay.TimePauseReal(user)
if len(workDay.Bookings) > 1 || !helper.IsWeekend(workDay.Date()) {
workDays = append(workDays, workDay)
} else if len(workDay.Bookings) > 0 || (workDay.Absence != Absence{}) {
workDays = append(workDays, workDay)
// } else {
// log.Println("no booking on day", workDay.Day.Format("02.01.2006"))
}
}
if err = rows.Err(); err != nil {
log.Println("Error in workday rows!", err)
return workDays
}
return workDays
}
func (d *WorkDay) calcPauseTime() {
if d.workTime > 6*time.Hour && d.pauseTime < 45*time.Minute {
if d.workTime <= (9*time.Hour) && d.pauseTime < 30*time.Minute {
diff := 30*time.Minute - d.pauseTime
d.workTime -= diff
d.pauseTime += diff
} else if d.pauseTime < 45*time.Minute {
diff := 45*time.Minute - d.pauseTime
d.workTime -= diff
d.pauseTime += diff
}
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)
}
// Gets the duration someone worked that day
func (d *WorkDay) getWorkTime() {
if len(d.Bookings) < 1 {
return
}
var workTime, pauseTime time.Duration
var lastBooking Booking
for _, booking := range d.Bookings {
if booking.CheckInOut%2 == 1 {
if !lastBooking.Timestamp.IsZero() {
pauseTime += booking.Timestamp.Sub(lastBooking.Timestamp)
}
} else {
workTime += booking.Timestamp.Sub(lastBooking.Timestamp)
}
lastBooking = booking
}
// checks if booking is today and has no gehen yet, so the time since last kommen booking is added to workTime
if d.Day.Day() == time.Now().Day() && len(d.Bookings)%2 == 1 {
workTime += time.Since(lastBooking.Timestamp.Local())
}
d.workTime = workTime
d.pauseTime = pauseTime
d.calcPauseTime()
}
func (d *WorkDay) GetWorkTimeString() (string, string) {
workString := helper.FormatDuration(d.workTime)
pauseString := helper.FormatDuration(d.pauseTime)
return workString, pauseString
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
func (d *WorkDay) RequiresAction() bool {
if len(d.Bookings) > 0 {
return d.Bookings[len(d.Bookings)-1].CheckInOut == 254
if len(d.Bookings) == 0 {
return false
}
return false
return d.Bookings[len(d.Bookings)-1].CheckInOut == 254
}
// returns a integer percentage of how much day has been worked of
func (d *WorkDay) GetWorkDayProgress(user User) uint8 {
defaultWorkTime := time.Duration(user.ArbeitszeitPerTag * float32(time.Hour)).Round(time.Minute)
progress := (d.workTime.Seconds() / defaultWorkTime.Seconds()) * 100
return uint8(progress)
func (d *WorkDay) GetDayProgress(u User) int8 {
if d.RequiresAction() {
return -1
}
workTime := d.TimeWorkVirtual(u)
progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100
return int8(progress)
}
func (d *WorkDay) CalcOvertime(user User) time.Duration {
if helper.IsWeekend(d.Day) && len(d.Bookings) == 0 {
return 0
}
var overtime time.Duration
overtime = d.workTime - time.Duration(user.ArbeitszeitPerTag*float32(time.Hour)).Round(time.Minute)
// weekday is WE
if (d.Absence != Absence{}) {
overtime = 0
}
return overtime
}
// 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

@@ -0,0 +1,81 @@
package models_test
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"log"
"testing"
"time"
)
func CatchError[T any](val T, err error) T {
if err != nil {
log.Fatalln(err)
}
return val
}
var testWorkDay = models.WorkDay{
Day: CatchError(time.Parse(time.DateOnly, "2025-01-01")),
Bookings: testBookings8hrs,
TimeFrom: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")),
TimeTo: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 16:30")),
}
func TestCalcRealWorkTime(t *testing.T) {
workTime := testWorkDay.TimeWorkReal(testUser)
if workTime != time.Hour*8 {
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
expectedWorkTime time.Duration
expectedPauseTime time.Duration
expectedOvertime time.Duration
}
testCases := []testCase{testCase{
Name: "6hrs no pause",
bookings: testBookings6hrs,
expectedWorkTime: 6 * time.Hour,
expectedPauseTime: 0,
expectedOvertime: -2 * time.Hour,
},
testCase{
Name: "8hrs - 30min pause",
bookings: testBookings8hrs,
expectedWorkTime: 7*time.Hour + 30*time.Minute,
expectedPauseTime: 30 * time.Minute,
expectedOvertime: -30 * time.Minute,
},
testCase{
Name: "10hrs - 45min pause",
bookings: testBookings10hrs,
expectedWorkTime: 9*time.Hour + 15*time.Minute,
expectedPauseTime: 45 * time.Minute,
expectedOvertime: 1*time.Hour + 15*time.Minute,
}}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
testWorkDay.Bookings = test.bookings
testWorkDay.TimeWorkReal(testUser)
testWorkDay.TimePauseReal(testUser)
testWorkDay.TimeOvertimeReal(testUser)
workTime, pauseTime, overTime := testWorkDay.GetAllWorkTimesReal(testUser)
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))
}
if overTime != test.expectedOvertime {
t.Errorf("Calculated wrong overtime: should be %s, but was %s", helper.FormatDuration(test.expectedOvertime), helper.FormatDuration(overTime))
}
})
}
}

View File

@@ -1,26 +1,28 @@
package models
import (
"arbeitszeitmessung/helper"
"database/sql"
"errors"
"log"
"log/slog"
"time"
"github.com/lib/pq"
)
// Workweeks are
type WorkWeek struct {
Id int
WorkDays []WorkDay
Absences []Absence
User User
WeekStart time.Time
Worktime time.Duration
Overtime time.Duration
Status WeekStatus
overtimeDiff time.Duration
worktimeDiff time.Duration
Id int
WorkDays []WorkDay
Absences []Absence
Days []IWorkDay
User User
WeekStart time.Time
Worktime time.Duration
WorkTimeVirtual time.Duration
Overtime time.Duration
Status WeekStatus
}
type WeekStatus int8
@@ -39,20 +41,23 @@ func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek {
Status: WeekStatusNone,
}
if populate {
week.PopulateWithBookings(0, 0)
week.PopulateWithDays(0, 0)
}
return week
}
func (w *WorkWeek) PopulateWithBookings(worktime time.Duration, overtime time.Duration) {
w.WorkDays = (*WorkDay).GetWorkDays(nil, w.User.CardUID, w.WeekStart, w.WeekStart.Add(7*24*time.Hour))
if absences, err := GetAbsencesByCardUID(w.User.CardUID, w.WeekStart, w.WeekStart.Add(7*24*time.Hour)); err == nil {
w.Absences = absences
} else {
log.Printf("Error populating absences in workWeek (%s): %v", w.WeekStart, err)
func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Duration) {
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)
for _, day := range w.Days {
work, _ := day.TimePauseReal(w.User)
w.Worktime += work
w.WorkTimeVirtual += day.TimeWorkVirtual(w.User)
}
w.Worktime = w.aggregateWorkTime()
w.Overtime = w.Worktime - time.Duration(w.User.ArbeitszeitPerWoche*float32(time.Hour))
slog.Debug("Got worktime for user", "user", w.User, "worktime", w.Worktime.String(), "virtualWorkTime", w.WorkTimeVirtual.String())
w.Overtime = w.WorkTimeVirtual - w.User.ArbeitszeitProWoche()
w.Worktime = w.Worktime.Round(time.Minute)
w.Overtime = w.Overtime.Round(time.Minute)
@@ -63,8 +68,6 @@ func (w *WorkWeek) PopulateWithBookings(worktime time.Duration, overtime time.Du
if overtime != w.Overtime || worktime != w.Worktime {
w.Status = WeekStatusDifferences
w.overtimeDiff = overtime
w.worktimeDiff = worktime
}
}
@@ -99,19 +102,16 @@ func (w *WorkWeek) CheckStatus() WeekStatus {
return w.Status
}
func (w *WorkWeek) GetWorkHourString() string {
return helper.FormatDuration(w.Worktime)
}
func (w *WorkWeek) aggregateWorkTime() time.Duration {
var workTime time.Duration
for _, day := range w.WorkDays {
workTime += day.workTime
}
for _, absences := range w.Absences {
absenceWorkTime := absences.AbwesenheitTyp.WorkTime - (absences.AbwesenheitTyp.WorkTime - w.User.ArbeitszeitPerTag) // workTime Equivalent of Absence is capped at user Worktime per Day
workTime += time.Duration(absenceWorkTime * float32(time.Hour)).Round(time.Minute)
}
// for _, absence := range w.Absences {
// log.Println(absence)
// absenceWorkTime := float32(8) // := absences.AbwesenheitTyp.WorkTime - (absences.AbwesenheitTyp.WorkTime - w.User.ArbeitszeitPerTag) // workTime Equivalent of Absence is capped at user Worktime per Day
// workTime += time.Duration(absenceWorkTime * float32(time.Hour)).Round(time.Minute)
// }
return workTime
}
@@ -132,12 +132,13 @@ func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek {
defer rows.Close()
for rows.Next() {
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)
return weeks
}
week.PopulateWithBookings(week.Worktime, week.Overtime)
week.PopulateWithDays(workTime, overTime)
weeks = append(weeks, week)
}
if err = rows.Err(); err != nil {
@@ -149,30 +150,70 @@ func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek {
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
func (w *WorkWeek) SendWeek() error {
var qStr *sql.Stmt
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 {
log.Println("Cannot send week, because it's the running week!")
slog.Warn("Cannot send week, because it's the running week!")
return ErrRunningWeek
}
if w.CheckStatus() != WeekStatusNone {
qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE, arbeitszeit = make_interval(secs => $3::numeric / 1000000000), ueberstunden = make_interval(secs => $4::numeric / 1000000000) 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 {
log.Println("Error preparing SQL statement", err)
slog.Warn("Error preparing SQL statement", "error", err)
return err
}
} else {
qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden) 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 {
log.Println("Error preparing SQL statement", err)
slog.Warn("Error preparing SQL statement", "error", 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 {
log.Println("Error executing query!", err)
return err
@@ -193,3 +234,11 @@ func (w *WorkWeek) Accept() error {
}
return nil
}
func (w *WorkWeek) RequiresAction() bool {
var requiresAction bool = false
for _, day := range w.Days {
requiresAction = requiresAction || day.RequiresAction()
}
return requiresAction
}

View File

@@ -8,7 +8,7 @@ import (
func SetupWorkWeekFixture(t *testing.T) models.WorkWeek {
t.Helper()
monday, err := time.Parse("2006-01-02", "2025-01-10")
monday, err := time.Parse(time.DateOnly, "2025-01-10")
if err != nil {
t.Fatal(err)
}
@@ -16,7 +16,7 @@ func SetupWorkWeekFixture(t *testing.T) models.WorkWeek {
}
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 {
t.Fatal(err)
}
@@ -28,6 +28,7 @@ func TestNewWorkWeekNoPopulate(t *testing.T) {
}
func TestCheckStatus(t *testing.T) {
SetupDBFixture(t)
testWeek := SetupWorkWeekFixture(t)
testCases := []struct {
name string

View File

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

View File

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

View File

@@ -9,16 +9,21 @@
"Courier New", monospace;
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325);
--color-red-700: oklch(50.5% 0.213 27.518);
--color-orange-500: oklch(70.5% 0.213 47.604);
--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-800: oklch(27.9% 0.041 260.031);
--color-neutral-100: oklch(97% 0 0);
--color-neutral-200: oklch(92.2% 0 0);
--color-neutral-300: oklch(87% 0 0);
--color-neutral-400: oklch(70.8% 0 0);
--color-neutral-500: oklch(55.6% 0 0);
--color-neutral-600: oklch(43.9% 0 0);
--color-neutral-700: oklch(37.1% 0 0);
--color-neutral-800: oklch(26.9% 0 0);
--color-neutral-900: oklch(20.5% 0 0);
--color-black: #000;
--color-white: #fff;
--spacing: 0.25rem;
@@ -189,6 +194,42 @@
.\@container {
container-type: inline-size;
}
.absolute {
position: absolute;
}
.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: calc(var(--spacing) * 2.5);
}
.top-\[0\.125rem\] {
top: 0.125rem;
}
.right-1 {
right: calc(var(--spacing) * 1);
}
.right-2 {
right: calc(var(--spacing) * 2);
}
.right-2\.5 {
right: calc(var(--spacing) * 2.5);
}
.left-1 {
left: calc(var(--spacing) * 1);
}
.left-1\/2 {
left: calc(1/2 * 100%);
}
.col-span-2 {
grid-column: span 2 / span 2;
}
@@ -207,13 +248,22 @@
.mt-1 {
margin-top: calc(var(--spacing) * 1);
}
.mb-1 {
margin-bottom: calc(var(--spacing) * 1);
}
.mb-2 {
margin-bottom: calc(var(--spacing) * 2);
}
.icon-\[material-symbols-light--add-circle-outline\] {
.ml-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.5em;
height: 1.5em;
width: 1.25em;
height: 1.25em;
background-color: currentColor;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
@@ -221,12 +271,12 @@
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='M11.5 16.5h1v-4h4v-1h-4v-4h-1v4h-4v1h4zm.503 4.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");
--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\] {
display: inline-block;
width: 1.5em;
height: 1.5em;
width: 1.25em;
height: 1.25em;
background-color: currentColor;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
@@ -238,8 +288,8 @@
}
.icon-\[material-symbols-light--circle-outline\] {
display: inline-block;
width: 1.5em;
height: 1.5em;
width: 1.25em;
height: 1.25em;
background-color: currentColor;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
@@ -249,10 +299,23 @@
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='M12.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--delete-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='M7.616 20q-.672 0-1.144-.472T6 18.385V6H5V5h4v-.77h6V5h4v1h-1v12.385q0 .69-.462 1.153T16.384 20zM17 6H7v12.385q0 .269.173.442t.443.173h8.769q.23 0 .423-.192t.192-.424zM9.808 17h1V8h-1zm3.384 0h1V8h-1zM7 6v13z'/%3E%3C/svg%3E");
}
.icon-\[material-symbols-light--more-time\] {
display: inline-block;
width: 1.5em;
height: 1.5em;
width: 1.25em;
height: 1.25em;
background-color: currentColor;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
@@ -264,8 +327,8 @@
}
.icon-\[material-symbols-light--motion-photos-paused-outline\] {
display: inline-block;
width: 1.5em;
height: 1.5em;
width: 1.25em;
height: 1.25em;
background-color: currentColor;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
@@ -275,6 +338,22 @@
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");
}
.icon-\[material-symbols-light--schedule-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='m15.646 16.354l.708-.708l-3.854-3.854V7h-1v5.208zM12.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.325 0 5.663-2.337T20 12t-2.337-5.663T12 4T6.337 6.338T4 12t2.338 5.663T12 20'/%3E%3C/svg%3E");
}
.block {
display: block;
}
.flex {
display: flex;
}
@@ -287,6 +366,9 @@
.inline {
display: inline;
}
.inline-flex {
display: inline-flex;
}
.table {
display: table;
}
@@ -298,12 +380,25 @@
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
.size-5 {
width: calc(var(--spacing) * 5);
height: calc(var(--spacing) * 5);
}
.h-2 {
height: calc(var(--spacing) * 2);
}
.h-3 {
height: calc(var(--spacing) * 3);
}
.h-3\.5 {
height: calc(var(--spacing) * 3.5);
}
.h-4 {
height: calc(var(--spacing) * 4);
}
.h-5 {
height: calc(var(--spacing) * 5);
}
.h-8 {
height: calc(var(--spacing) * 8);
}
@@ -316,9 +411,18 @@
.w-2 {
width: calc(var(--spacing) * 2);
}
.w-3 {
width: calc(var(--spacing) * 3);
}
.w-3\.5 {
width: calc(var(--spacing) * 3.5);
}
.w-4 {
width: calc(var(--spacing) * 4);
}
.w-5 {
width: calc(var(--spacing) * 5);
}
.w-9 {
width: calc(var(--spacing) * 9);
}
@@ -343,27 +447,70 @@
.flex-grow {
flex-grow: 1;
}
.grow {
flex-grow: 1;
.grow-0 {
flex-grow: 0;
}
.grow-1 {
flex-grow: 1;
}
.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;
}
.resize {
resize: both;
}
.scroll-m-2 {
scroll-margin: calc(var(--spacing) * 2);
}
.appearance-none {
appearance: none;
}
.break-after-page {
break-after: page;
}
.auto-rows-min {
grid-auto-rows: min-content;
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.grid-cols-\[3fr_2fr_2fr_2fr_3fr_3fr_3fr\] {
grid-template-columns: 3fr 2fr 2fr 2fr 3fr 3fr 3fr;
}
.grid-cols-subgrid {
grid-template-columns: subgrid;
}
.grid-rows-6 {
grid-template-rows: repeat(6, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}
@@ -379,6 +526,9 @@
.items-center {
align-items: center;
}
.items-end {
align-items: flex-end;
}
.justify-around {
justify-content: space-around;
}
@@ -411,18 +561,29 @@
border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
}
}
.divide-neutral-300 {
:where(& > :not(:last-child)) {
border-color: var(--color-neutral-300);
}
}
.justify-self-end {
justify-self: flex-end;
}
.overflow-hidden {
overflow: hidden;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-full {
border-radius: calc(infinity * 1px);
}
.rounded-md {
border-radius: var(--radius-md);
}
.rounded-none {
border-radius: 0;
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
@@ -431,14 +592,39 @@
border-style: var(--tw-border-style);
border-width: 0px;
}
.border-neutral-200 {
border-color: var(--color-neutral-200);
.border-r-0 {
border-right-style: var(--tw-border-style);
border-right-width: 0px;
}
.border-r-1 {
border-right-style: var(--tw-border-style);
border-right-width: 1px;
}
.border-b-0 {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 0px;
}
.border-dashed {
--tw-border-style: dashed;
border-style: dashed;
}
.border-neutral-300 {
border-color: var(--color-neutral-300);
}
.border-neutral-900 {
border-color: var(--color-neutral-900);
.border-neutral-500 {
border-color: var(--color-neutral-500);
}
.border-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 {
background-color: var(--color-accent);
@@ -452,9 +638,6 @@
.bg-neutral-400 {
background-color: var(--color-neutral-400);
}
.bg-neutral-700 {
background-color: var(--color-neutral-700);
}
.bg-orange-500 {
background-color: var(--color-orange-500);
}
@@ -473,6 +656,9 @@
.p-2 {
padding: calc(var(--spacing) * 2);
}
.p-8 {
padding: calc(var(--spacing) * 8);
}
.px-3 {
padding-inline: calc(var(--spacing) * 3);
}
@@ -507,6 +693,12 @@
.text-accent {
color: var(--color-accent);
}
.text-black {
color: var(--color-black);
}
.text-neutral-300 {
color: var(--color-neutral-300);
}
.text-neutral-500 {
color: var(--color-neutral-500);
}
@@ -522,12 +714,24 @@
.text-red-600 {
color: var(--color-red-600);
}
.text-slate-600 {
color: var(--color-slate-600);
}
.text-slate-700 {
color: var(--color-slate-700);
}
.text-white {
color: var(--color-white);
}
.uppercase {
text-transform: uppercase;
}
.underline {
text-decoration-line: underline;
}
.opacity-0 {
opacity: 0%;
}
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
@@ -540,6 +744,11 @@
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
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-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));
@@ -549,6 +758,22 @@
--tw-duration: 300ms;
transition-duration: 300ms;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
.\*\:text-center {
:is(& > *) {
text-align: center;
}
}
.\*\:not-print\:p-2 {
:is(& > *) {
@media not print {
padding: calc(var(--spacing) * 2);
}
}
}
.group-hover\:text-black {
&:is(:where(.group):hover *) {
@media (hover: hover) {
@@ -563,9 +788,9 @@
}
}
}
.group-\[\.edit\]\:block {
.group-\[\.edit\]\:ml-2 {
&:is(:where(.group):is(.edit) *) {
display: block;
margin-left: calc(var(--spacing) * 2);
}
}
.group-\[\.edit\]\:flex {
@@ -583,16 +808,34 @@
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 {
color: var(--color-neutral-400);
}
}
.hover\:border-neutral-300 {
&:hover {
@media (hover: hover) {
border-color: var(--color-neutral-300);
}
.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 {
@@ -616,17 +859,10 @@
}
}
}
.hover\:bg-red-600 {
.hover\:bg-red-700 {
&:hover {
@media (hover: hover) {
background-color: var(--color-red-600);
}
}
}
.hover\:text-accent {
&:hover {
@media (hover: hover) {
color: var(--color-accent);
background-color: var(--color-red-700);
}
}
}
@@ -637,11 +873,6 @@
}
}
}
.focus\:border-neutral-400 {
&:focus {
border-color: var(--color-neutral-400);
}
}
.focus\:bg-neutral-700 {
&:focus {
background-color: var(--color-neutral-700);
@@ -668,11 +899,6 @@
opacity: 50%;
}
}
.max-md\:flex {
@media (width < 48rem) {
display: flex;
}
}
.max-md\:grid {
@media (width < 48rem) {
display: grid;
@@ -683,11 +909,6 @@
display: none;
}
}
.max-md\:flex-col {
@media (width < 48rem) {
flex-direction: column;
}
}
.max-md\:divide-y-1 {
@media (width < 48rem) {
:where(& > :not(:last-child)) {
@@ -699,18 +920,6 @@
}
}
}
.max-md\:border-0 {
@media (width < 48rem) {
border-style: var(--tw-border-style);
border-width: 0px;
}
}
.max-md\:border-b-1 {
@media (width < 48rem) {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px;
}
}
.max-md\:bg-neutral-300 {
@media (width < 48rem) {
background-color: var(--color-neutral-300);
@@ -726,11 +935,6 @@
grid-column: span 3 / span 3;
}
}
.md\:col-span-4 {
@media (width >= 48rem) {
grid-column: span 4 / span 4;
}
}
.md\:mx-\[10\%\] {
@media (width >= 48rem) {
margin-inline: 10%;
@@ -756,6 +960,11 @@
width: calc(1/2 * 100%);
}
}
.md\:flex-row {
@media (width >= 48rem) {
flex-direction: row;
}
}
.md\:px-4 {
@media (width >= 48rem) {
padding-inline: calc(var(--spacing) * 4);
@@ -766,8 +975,8 @@
color: transparent;
}
}
.group-\[\.edit\]\:md\:block {
&:is(:where(.group):is(.edit) *) {
.group-\[\.edit\]\/button\:md\:block {
&:is(:where(.group\/button):is(.edit) *) {
@media (width >= 48rem) {
display: block;
}
@@ -815,10 +1024,17 @@
}
}
}
@layer base {
body {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
background-color: white;
}
}
@layer components {
.grid-main {
display: grid;
grid-template-columns: 2fr auto 1fr;
grid-template-columns: 4fr 3fr 3fr 1fr;
align-items: stretch;
}
.grid-sub {
@@ -851,9 +1067,12 @@
border-width: 1px;
border-color: var(--color-neutral-800);
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-timing-function: var( --tw-ease, var(--default-transition-timing-function) );
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
input.btn, select.btn {
transition-duration: 300ms;
}
.btn:hover {
color: var(--color-white);
background-color: var(--color-neutral-700);
@@ -862,8 +1081,38 @@
opacity: 50%;
pointer-events: none;
}
.btn:active, .btn:focus {
background-color: var(--color-neutral-700);
input.btn, select.btn {
text-align: left;
}
input.btn:hover, select.btn:hover {
border-color: var(--color-neutral-300);
background-color: var(--color-neutral-100);
color: var(--color-neutral-800);
}
.edit-box {
border-radius: var(--radius-md);
overflow: hidden;
border-color: var(--color-neutral-500);
transition-property: background-color, border-color;
transition-timing-function: var(--default-transition-timing-function) * 2;
transition-duration: var(--default-transition-duration);
outline: none;
&:is(:where(.group):is(.edit) *) {
border-width: 1px;
}
}
.edit-box:hover {
&:is(:where(.group):is(.edit) *) {
background-color: var(--color-white);
border-color: var(--color-neutral-300);
}
}
.edit-box input:focus {
outline: none;
}
div.edit {
border-width: 1px;
background-color: var(--color-neutral-300);
}
@media (width >=48rem) {
.grid-main {
@@ -878,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 {
syntax: "*";
inherits: false;
@@ -962,6 +1246,14 @@
@layer properties {
@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 {
--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-border-style: solid;
--tw-divide-y-reverse: 0;

BIN
Backend/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,34 +1,87 @@
function editDay(element, event, formId) {
var form = element.closest(".grid-sub").querySelector(".all-booking-component > form");
form.classList.toggle("edit");
element.classList.toggle("edit");
if (element.classList.contains("edit")) {
event.preventDefault();
form.querySelectorAll("input, select").forEach((input) => {
input.disabled = false;
});
} else {
form.submit();
function clearEditState() {
for (let e of document.querySelectorAll(".edit")) {
e.classList.remove("edit");
}
toggleAbsenceEdit(false);
}
function clearButtonState() {
for (let b of document.querySelectorAll(".change-button-component")) {
b.type = "button";
}
}
function editAbwesenheit(element, event) {
var newBookingComponent = element.closest(".grid-sub").querySelector(".new-booking-component");
if (element.value == 0) {
newBookingComponent.style.display = "";
function editWorkday(element, event, id, isWorkDay) {
event.preventDefault();
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;
}
clearButtonState();
element.type = "submit";
} else {
form.submit();
}
} else {
newBookingComponent.style.display = "none";
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) {
for (let field of fieldsToSync) {
const src = from.querySelector(`[name=${field}]`);
const target = to.querySelector(`[name=${field}]`);
if (!src || !target) return;
target.value = src.value;
}
}
function navigateWeek(element, event, direction) {
var dateInput = element.closest("form").querySelector("input[type=date]");
var date = dateInput.valueAsDate;
const dateInput = element.closest("form").querySelector("input[type=date]");
const date = dateInput.valueAsDate;
date.setDate(date.getDate() + 7 * direction);
date.setHours(10);
dateInput.valueAsDate = date;
}
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">
<a href="/time">Zeitverwaltung</a>
<a href="/team">Abrechnung</a>
<a href="/pdf">PDF</a>
if true {
<a href="/team/presence">Anwesenheit</a>
<a href="/presence">Anwesenheit</a>
}
<a href="/user/settings">Einstellungen</a>
@LogoutButton()

View File

@@ -29,12 +29,12 @@ func headerComponent() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex flex-row justify-between md:mx-[10%] py-2 items-center\"><a href=\"/time\">Zeitverwaltung</a> <a href=\"/team\">Abrechnung</a> ")
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 {
return templ_7745c5c3_Err
}
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 {
return templ_7745c5c3_Err
}

View File

@@ -1,12 +1,6 @@
package templates
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"fmt"
"strconv"
"time"
)
import "arbeitszeitmessung/models"
templ Base() {
<!DOCTYPE html>
@@ -18,21 +12,6 @@ templ Base() {
</head>
}
templ TimePage(workDays []models.WorkDay, lastSub time.Time) {
@Base()
@headerComponent()
<div class="grid-main divide-y-1">
@inputForm()
for _, day := range workDays {
@dayComponent(day, day.Day.Before(lastSub))
if (day.Day.Weekday() == time.Monday) {
<div class="grid-sub responsive bg-neutral-300 h-2"></div>
}
}
</div>
@LegendComponent()
}
templ LoginPage(success bool, errorMsg string) {
@Base()
<div class="w-full h-[100vh] flex flex-col justify-center items-center">
@@ -49,7 +28,10 @@ templ LoginPage(success bool, errorMsg string) {
</div>
}
templ UserPage(status int) {
templ SettingsPage(status int) {
{{
user := ctx.Value("user").(models.User)
}}
@Base()
@headerComponent()
<div class="grid-main divide-y-1">
@@ -72,6 +54,14 @@ templ UserPage(status int) {
<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">{ 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">
<h1 class="grid-cell font-bold uppercase text-xl text-center">Nutzer abmelden</h1>
<div class="grid-cell col-span-3">
@@ -95,99 +85,24 @@ templ statusCheckMark(status models.WeekStatus, target models.WeekStatus) {
templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) {
@Base()
@headerComponent()
{{
progress := (float32(userWeek.Worktime.Hours()) / userWeek.User.ArbeitszeitPerWoche) * 100
}}
<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 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(uint8(progress), false, false)
<div>
<p>Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDuration(userWeek.Worktime)) }</p>
<p>Überstunden: { fmt.Sprintf("%s", helper.FormatDuration(userWeek.Overtime)) }</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.WorkDays {
@weekDayComponent(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 } type="submit" class="btn">Senden</button>
</form>
<h2 class="text-xl uppercase font-bold">Eigene Abrechnung</h2>
</div>
</div>
@workWeekComponent(userWeek, false)
if len(weeks) > 0 {
<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>
}
for _, week := range weeks {
@employeComponent(week)
@workWeekComponent(week, true)
}
</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() {
<button onclick="logoutUser()" type="button" class="cursor-pointer">Abmelden</button>
}

View File

@@ -8,13 +8,7 @@ package templates
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"fmt"
"strconv"
"time"
)
import "arbeitszeitmessung/models"
func Base() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
@@ -45,7 +39,7 @@ func Base() templ.Component {
})
}
func TimePage(workDays []models.WorkDay, lastSub time.Time) templ.Component {
func LoginPage(success bool, errorMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -70,95 +64,30 @@ func TimePage(workDays []models.WorkDay, lastSub time.Time) templ.Component {
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, 2, "<div class=\"grid-main divide-y-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = inputForm().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, day := range workDays {
templ_7745c5c3_Err = dayComponent(day, day.Day.Before(lastSub)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if day.Day.Weekday() == time.Monday {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"grid-sub responsive bg-neutral-300 h-2\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = LegendComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func LoginPage(success bool, errorMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"w-full h-[100vh] flex flex-col justify-center items-center\"><form method=\"POST\" class=\"w-9/10 md:w-1/2 flex flex-col gap-4 p-2 mb-2\"><h1 class=\"font-bold uppercase text-xl text-center mb-2\">Benutzer Anmelden</h1><input name=\"personal_nummer\" placeholder=\"Personalnummer\" type=\"text\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> <input name=\"password\" placeholder=\"Passwort\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full h-[100vh] flex flex-col justify-center items-center\"><form method=\"POST\" class=\"w-9/10 md:w-1/2 flex flex-col gap-4 p-2 mb-2\"><h1 class=\"font-bold uppercase text-xl text-center mb-2\">Benutzer Anmelden</h1><input name=\"personal_nummer\" placeholder=\"Personalnummer\" type=\"text\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> <input name=\"password\" placeholder=\"Passwort\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !success {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"text-red-600 text-sm\">Login fehlgeschlagen, bitte erneut versuchen!</p><p class=\"text-red-600 text-sm\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<p class=\"text-red-600 text-sm\">Login fehlgeschlagen, bitte erneut versuchen!</p><p class=\"text-red-600 text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(errorMsg)
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errorMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 45, Col: 46}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 24, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<button type=\"submit\" class=\"cursor-pointer rounded-md text-neutral-800 p-2 md:px-4 border text-center text-sm hover:text-white transition-colors border-neutral-300 focus:bg-neutral-700 active:bg-neutral-700 hover:bg-neutral-700 disabled:pointer-events-none disabled:opacity-50\">Login</button></form></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button type=\"submit\" class=\"cursor-pointer rounded-md text-neutral-800 p-2 md:px-4 border text-center text-sm hover:text-white transition-colors border-neutral-300 focus:bg-neutral-700 active:bg-neutral-700 hover:bg-neutral-700 disabled:pointer-events-none disabled:opacity-50\">Login</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -166,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) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -182,11 +111,13 @@ func UserPage(status int) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
user := ctx.Value("user").(models.User)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
@@ -195,28 +126,67 @@ func UserPage(status int) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"grid-main divide-y-1\"><form method=\"POST\" class=\"grid-sub responsive lg:divide-x-1\"><h1 class=\"grid-cell font-bold uppercase text-xl text-center\">Passwort ändern</h1><div class=\"grid-cell col-span-3 flex flex-col gap-2\"><input name=\"password\" placeholder=\"Aktuelles Passwort\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> <input name=\"new_password\" placeholder=\"Neues Passwort\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> <input name=\"new_password_repeat\" placeholder=\"Neues Passwort wiederholen\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"grid-main divide-y-1\"><form method=\"POST\" class=\"grid-sub responsive lg:divide-x-1\"><h1 class=\"grid-cell font-bold uppercase text-xl text-center\">Passwort ändern</h1><div class=\"grid-cell col-span-3 flex flex-col gap-2\"><input name=\"password\" placeholder=\"Aktuelles Passwort\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> <input name=\"new_password\" placeholder=\"Neues Passwort\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> <input name=\"new_password_repeat\" placeholder=\"Neues Passwort wiederholen\" type=\"password\" class=\"w-full placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-300 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none hover:border-neutral-500\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
switch {
case status == 401:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p class=\"text-red-600 text-sm\">Aktuelles Passwort nicht korrekt!</p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"text-red-600 text-sm\">Aktuelles Passwort nicht korrekt!</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case status >= 400:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<p class=\"text-red-600 text-sm\">Passwortwechsel fehlgeschlagen, bitte erneut versuchen!</p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p class=\"text-red-600 text-sm\">Passwortwechsel fehlgeschlagen, bitte erneut versuchen!</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case status == 202:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<p class=\"text-accent text-sm\">Passwortänderung erfolgreich</p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"text-accent text-sm\">Passwortänderung erfolgreich</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</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 {
return templ_7745c5c3_Err
}
@@ -240,18 +210,18 @@ func statusCheckMark(status models.WeekStatus, target models.WeekStatus) templ.C
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if status >= target {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<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 {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<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 {
return templ_7745c5c3_Err
}
@@ -276,9 +246,9 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
@@ -289,238 +259,27 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
progress := (float32(userWeek.Worktime.Hours()) / userWeek.User.ArbeitszeitPerWoche) * 100
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<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 class=\"grid-cell flex flex-col max-md:border-b-1 max-md:bg-neutral-300 gap-2 \"><div class=\"lg:hidden\">")
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 {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = weekPicker(userWeek.WeekStart).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div><h2 class=\"uppercase font-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s %s", userWeek.User.Vorname, userWeek.User.Name))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 110, Col: 101}
}
_, 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, 19, "</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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = statusCheckMark(userWeek.CheckStatus(), models.WeekStatusSent).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "Gesendet</span> <span class=\"flex flex-row gap-2 items-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = statusCheckMark(userWeek.CheckStatus(), models.WeekStatusAccepted).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "Akzeptiert</span></div><div class=\"flex flex-row gap-2 col-span-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = timeGaugeComponent(uint8(progress), false, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div><p>Arbeitszeit: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDuration(userWeek.Worktime)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 125, Col: 84}
}
_, 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, 23, "</p><p>Überstunden: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDuration(userWeek.Overtime)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 126, Col: 85}
}
_, 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, 24, "</p></div></div></div></div><div class=\"grid-cell col-span-3 flex flex-col @7xl:grid @7xl:grid-cols-5 gap-2 py-4 content-baseline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, day := range userWeek.WorkDays {
templ_7745c5c3_Err = weekDayComponent(userWeek.User, day).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div><div class=\"grid-cell flex flex-col gap-2 justify-between\"><div class=\"max-md:hidden\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = weekPicker(userWeek.WeekStart).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div><form method=\"post\" class=\"flex flex-col gap-2\"><input type=\"hidden\" name=\"method\" value=\"send\"> <input type=\"hidden\" name=\"user\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(userWeek.User.PersonalNummer))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 142, Col: 88}
}
_, 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, 27, "\"> <input type=\"hidden\" name=\"week\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(userWeek.WeekStart.Format(time.DateOnly))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 143, Col: 86}
}
_, 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, 28, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
switch userWeek.CheckStatus() {
case models.WeekStatusNone:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<p class=\"text-sm\">an Vorgesetzten senden</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case models.WeekStatusSent:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<p class=\"text-sm\">an Vorgesetzten gesendet</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case models.WeekStatusAccepted:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<p class=\"text-sm\">vom Vorgesetzten bestätigt</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<button")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if userWeek.Status < models.WeekStatusSent {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " type=\"submit\" class=\"btn\">Korrigieren</button> <button")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if time.Since(userWeek.WeekStart) < 24*7*time.Hour || userWeek.Status >= models.WeekStatusSent {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " type=\"submit\" class=\"btn\">Senden</button></form></div></div>")
templ_7745c5c3_Err = workWeekComponent(userWeek, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(weeks) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<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 {
return templ_7745c5c3_Err
}
}
for _, week := range weeks {
templ_7745c5c3_Err = employeComponent(week).Render(ctx, templ_7745c5c3_Buffer)
templ_7745c5c3_Err = workWeekComponent(week, true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</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_Var13 := templ.GetChildren(ctx)
if templ_7745c5c3_Var13 == nil {
templ_7745c5c3_Var13 = 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, 39, "<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, 40, "</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, 41, "</div></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -544,12 +303,12 @@ func LogoutButton() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var14 := templ.GetChildren(ctx)
if templ_7745c5c3_Var14 == nil {
templ_7745c5c3_Var14 = templ.NopComponent
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

134
Backend/templates/pdf.templ Normal file
View File

@@ -0,0 +1,134 @@
package templates
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"fmt"
"time"
)
templ PDFForm(teamMembers []models.User) {
@Base()
@headerComponent()
<div class="grid-main divide-y-1">
<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="" class="btn bg-neutral-100"/>
</div>
<div></div>
</div>
<div class="grid-sub divide-x-1 responsive">
<div class="grid-cell">Mitarbeiter wählen</div>
<div class="grid-cell col-span-3 flex flex-col gap-2">
<div class="flex flex-row gap-2">
<button class="btn" type="button" onclick={ templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("true")) }>Alle</button>
<button class="btn" type="button" onclick={ templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("false")) }>Keine</button>
</div>
for _, member := range teamMembers {
@CheckboxComponent(fmt.Sprintf("pdf-%d", member.PersonalNummer), fmt.Sprintf("%s %s", member.Vorname, member.Name))
}
</div>
<div></div>
</div>
<div class="grid-sub divide-x-1 responsive">
<div class="grid-cell">PDFs Bündeln</div>
<div class="grid-cell col-span-3 flex gap-2 flex-col md:flex-row">
<button class="btn" type="button" name="action" value="download">Einzeln</button>
<button class="btn" type="button" name="action" value="preview" onclick="">Bündel</button>
</div>
</div>
</div>
}
templ CheckboxComponent(id, label string) {
<div class="inline-flex items-center">
<label class="flex items-center cursor-pointer relative" for={ id }>
<input type="checkbox" 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" id={ id }/>
<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.GetAllWorkTimesVirtual(e) }}
@ColorDuration(work, noBorder)
@ColorDuration(pause, noBorder)
@ColorDuration(overtime, noBorder+" border-r-0")
if day.Date().Weekday() == time.Friday {
<p class="col-span-full bg-neutral-300">Wochenende</p>
}
}
</div>
</content>
}
templ ColorDuration(d time.Duration, classes string) {
{{
color := ""
if d.Abs() < time.Minute {
color = "text-neutral-300"
}
}}
<p class={ color + " " + classes }>{ helper.FormatDurationFill(d, true) }</p>
}

View File

@@ -0,0 +1,581 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.924
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"fmt"
"time"
)
func PDFForm(teamMembers []models.User) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"grid-main divide-y-1\"><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=\"\" class=\"btn bg-neutral-100\"></div><div></div></div><div class=\"grid-sub divide-x-1 responsive\"><div class=\"grid-cell\">Mitarbeiter wählen</div><div class=\"grid-cell col-span-3 flex flex-col gap-2\"><div class=\"flex flex-row gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("true")))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<button class=\"btn\" type=\"button\" onclick=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.ComponentScript = templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("true"))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2.Call)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">Alle</button> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("false")))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<button class=\"btn\" type=\"button\" onclick=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.ComponentScript = templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("false"))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3.Call)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">Keine</button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, member := range teamMembers {
templ_7745c5c3_Err = CheckboxComponent(fmt.Sprintf("pdf-%d", member.PersonalNummer), fmt.Sprintf("%s %s", member.Vorname, member.Name)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div></div></div><div class=\"grid-sub divide-x-1 responsive\"><div class=\"grid-cell\">PDFs Bündeln</div><div class=\"grid-cell col-span-3 flex gap-2 flex-col md:flex-row\"><button class=\"btn\" type=\"button\" name=\"action\" value=\"download\">Einzeln</button> <button class=\"btn\" type=\"button\" name=\"action\" value=\"preview\" onclick=\"\">Bündel</button></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func CheckboxComponent(id, 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_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<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_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 50, Col: 67}
}
_, 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, 8, "\"><input type=\"checkbox\" 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\" id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 51, Col: 178}
}
_, 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, "\"> <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_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 57, Col: 81}
}
_, 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, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 57, Col: 91}
}
_, 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, "</label></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, kw := tsStart.ISOWeek()
noBorder := ""
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<content class=\"p-8 relative flex flex-col gap-4 break-after-page\"><div><h1 class=\"text-2xl font-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vorname)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 69, Col: 45}
}
_, 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, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 69, Col: 56}
}
_, 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, 14, "</h1><p>Zeitraum: <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(tsStart.Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 70, Col: 52}
}
_, 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, 15, "</span> - <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, 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: 70, Col: 98}
}
_, 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, 16, "</span></p><p>Arbeitszeit: <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(worktime))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 71, Col: 58}
}
_, 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, 17, "</span></p><p>Überstunden: <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(overtime))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 72, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(kw)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 75, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for index, day := range workDays {
if index == len(workDays)-1 {
noBorder = "border-b-0"
}
var templ_7745c5c3_Var17 = []any{noBorder}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<p class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var17).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 88, Col: 59}
}
_, 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, 22, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 = []any{"grid grid-cols-subgrid col-span-3 " + noBorder}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var20).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_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">")
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, 25, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, 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: 95, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</p><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, 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: 96, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</p><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, 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: 97, Col: 55}
}
_, 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, 28, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if workDay.IsKurzArbeit() {
timeFrom, timeTo := workDay.GenerateKurzArbeitBookings(e)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(timeFrom.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 103, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</p><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(timeTo.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 104, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</p><p>Kurzarbeit</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
absentDay, _ := day.(*models.Absence)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<p class=\"col-span-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, 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: 111, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</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, 36, " ")
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, 37, " ")
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, 38, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if day.Date().Weekday() == time.Friday {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<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, 40, "</div></content>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func ColorDuration(d time.Duration, classes string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var28 := templ.GetChildren(ctx)
if templ_7745c5c3_Var28 == nil {
templ_7745c5c3_Var28 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
color := ""
if d.Abs() < time.Minute {
color = "text-neutral-300"
}
var templ_7745c5c3_Var29 = []any{color + " " + classes}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<p class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var29).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_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDurationFill(d, true))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 133, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -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.924
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

@@ -26,75 +26,140 @@ templ weekPicker(weekStart time.Time) {
</svg>
</button>
</form>
if time.Since(weekStart) < 24*7*time.Hour {
<p class="text-sm text-red-500">Die Woche kann erst am nächsten Montag gesendet werden!</p>
}
}
templ weekDayComponent(user models.User, day models.WorkDay) {
{{ work, pause := day.GetWorkTimeString() }}
templ defaultWeekDayComponent(u models.User, day models.IWorkDay) {
<div class="flex flex-row gap-2">
@timeGaugeComponent(day.GetWorkDayProgress(user), false, false)
@timeGaugeComponent(day.GetDayProgress(u), false)
<div class="flex flex-col">
<p class=""><span class="font-bold uppercase hidden md:inline">{ day.Day.Format("Mon") }:</span> { day.Day.Format("02.01.2006") }</p>
<div class="flex flex-row gap-2">
<span class="text-accent">{ work }</span>
<span class="text-neutral-500">{ pause }</span>
</div>
<div class="flex flex-row gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="size-4" viewBox="0 0 16 16">
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z"></path>
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0"></path>
</svg>
if day.Absence.Datum.Equal(day.Day) {
<p>{ day.Absence.AbwesenheitTyp.Name }</p>
} else if !day.TimeFrom.Equal(day.TimeTo) {
<span>{ day.TimeFrom.Format("15:04") }</span>
<span>-</span>
<span>{ day.TimeTo.Format("15:04") }</span>
<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() {
{{
workDay, _ := day.(*models.WorkDay)
work, pause, _ := workDay.GetAllWorkTimesReal(u)
}}
if !workDay.RequiresAction() {
<div class="flex flex-row gap-2">
<span class="text-accent">{ helper.FormatDuration(work) }</span>
<span class="text-neutral-500">{ helper.FormatDuration(pause) }</span>
</div>
<div class="flex flex-row gap-2 items-center">
<span class="icon-[material-symbols-light--schedule-outline] flex-shrink-0"></span>
switch {
case !workDay.TimeFrom.Equal(workDay.TimeTo):
<span>{ workDay.TimeFrom.Format("15:04") }</span>
<span>-</span>
<span>{ workDay.TimeTo.Format("15:04") }</span>
default:
<p>Keine Anwesenheit</p>
}
</div>
} else {
<p>Keine Anwesenheit</p>
<p class="text-red-600">Bitte anpassen</p>
}
</div>
} else {
{{
absentDay, _ := day.(*models.Absence)
}}
<div>{ absentDay.AbwesenheitTyp.Name } </div>
}
</div>
</div>
}
templ employeComponent(week models.WorkWeek) {
templ weekDayComponent(user models.User, day models.WorkDay) {
// {{ work, pause, _ := day.GetAllWorkTimesReal(user) }}
<div class="flex flex-row gap-2">
// @timeGaugeComponent(day.GetWorkDayProgress(user), false, day.RequiresAction())
<div class="flex flex-col">
if !day.RequiresAction() {
}
</div>
</div>
}
templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
{{
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="grid-cell flex flex-col max-md:bg-neutral-300 gap-2">
if !onlyAccept {
<div class="lg:hidden">
@weekPicker(week.WeekStart)
</div>
}
<p class="font-bold uppercase">{ week.User.Vorname } { week.User.Name }</p>
<div class="flex flex-row gap-2 col-span-3">
@timeGaugeComponent(uint8(progress), false, false)
<div>
<p>Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDuration(week.Worktime)) }</p>
<p>Überstunden: { fmt.Sprintf("%s", helper.FormatDuration(week.Overtime)) }</p>
<div class="grid grid-cols-5 gap-2 lg:grid-cols-1">
if !onlyAccept {
<div class="col-span-2">
<span class="flex flex-row gap-2 items-center">
@statusCheckMark(week.CheckStatus(), models.WeekStatusSent)
Gesendet
</span>
<span class="flex flex-row gap-2 items-center">
@statusCheckMark(week.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(week.Worktime)) }</p>
<p>Überstunden: { fmt.Sprintf("%s", helper.FormatDurationFill(week.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 content-baseline">
for _, day := range week.WorkDays {
@weekDayComponent(week.User, day)
<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 week.Days {
@defaultWeekDayComponent(week.User, day)
}
</div>
<form class="grid-cell flex flex-col justify-between gap-2" method="post">
<p class="text-sm"><span class="">Woche:</span> { fmt.Sprintf("%02d-%d", kw, year) }</p>
<input type="hidden" name="method" value="accept"/>
<input type="hidden" name="user" value={ strconv.Itoa(week.User.PersonalNummer) }/>
<input type="hidden" name="week" value={ week.WeekStart.Format(time.DateOnly) }/>
<div class="flex flex-col gap-2">
if week.Status == models.WeekStatusDifferences {
<p class="text-red-600 text-sm">Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen</p>
<div class="grid-cell flex flex-col gap-2 justify-between">
if onlyAccept {
<p class="text-sm"><span class="">Woche:</span> { fmt.Sprintf("%02d-%d", kw, year) }</p>
} else {
<div class="max-md:hidden">
@weekPicker(week.WeekStart)
</div>
}
<form method="post" class="flex flex-col gap-2">
{{
week.CheckStatus()
method := "accept"
if !onlyAccept {
method = "send"
}
}}
<input type="hidden" name="method" value={ method }/>
<input type="hidden" name="user" value={ strconv.Itoa(week.User.PersonalNummer) }/>
<input type="hidden" name="week" value={ week.WeekStart.Format(time.DateOnly) }/>
if onlyAccept {
if week.Status == models.WeekStatusDifferences {
<p class="text-red-600 text-sm">Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen</p>
}
<button type="submit" disabled?={ week.Status == models.WeekStatusDifferences } class="btn">Bestätigen</button>
} else {
switch {
case week.RequiresAction():
<p class="text-sm text-red-500">bitte zuerst Buchungen anpassen</p>
case time.Since(week.WeekStart) < 24*7*time.Hour:
<p class="text-sm text-red-500">Die Woche kann erst am nächsten Montag gesendet werden!</p>
case week.Status == models.WeekStatusNone:
<p class="text-sm">an Vorgesetzten senden</p>
case week.Status == models.WeekStatusSent:
<p class="text-sm">an Vorgesetzten gesendet</p>
case week.Status == models.WeekStatusAccepted:
<p class="text-sm">vom Vorgesetzten bestätigt</p>
}
<button disabled?={ week.Status < models.WeekStatusSent } type="submit" class="btn">Korrigieren</button>
<button disabled?={ time.Since(week.WeekStart) < 24*7*time.Hour || week.Status >= models.WeekStatusSent || week.RequiresAction() } type="submit" class="btn">Senden</button>
}
<button type="submit" disabled?={ week.Status == models.WeekStatusDifferences } class="btn">Bestätigen</button>
// TODO maybe delete function
// <button type="button" disabled?={ week.Status < models.WeekStatusDifferences } class="hover:bg-red-600 btn">Antrag löschen</button>
</div>
</form>
</form>
</div>
</div>
}

View File

@@ -113,11 +113,176 @@ func weekPicker(weekStart time.Time) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if time.Since(weekStart) < 24*7*time.Hour {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<p class=\"text-sm text-red-500\">Die Woche kann erst am nächsten Montag gesendet werden!</p>")
return nil
})
}
func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"flex flex-row gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = timeGaugeComponent(day.GetDayProgress(u), false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"flex flex-col\"><p class=\"\"><span class=\"font-bold uppercase hidden md:inline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatGermanDayOfWeek(day.Date()))
if templ_7745c5c3_Err != nil {
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))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ":</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 152}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if day.IsWorkDay() {
workDay, _ := day.(*models.WorkDay)
work, pause, _ := workDay.GetAllWorkTimesReal(u)
if !workDay.RequiresAction() {
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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(work))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 43, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span> <span class=\"text-neutral-500\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(pause))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 44, Col: 67}
}
_, 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, 16, "</span></div><div class=\"flex flex-row gap-2 items-center\"><span class=\"icon-[material-symbols-light--schedule-outline] flex-shrink-0\"></span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
switch {
case !workDay.TimeFrom.Equal(workDay.TimeTo):
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.TimeFrom.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 50, Col: 48}
}
_, 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, 18, "</span> <span>-</span> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.TimeTo.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 52, Col: 46}
}
_, 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, 19, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<p>Keine Anwesenheit</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p class=\"text-red-600\">Bitte anpassen</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
absentDay, _ := day.(*models.Absence)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div>")
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/teamComponents.templ`, Line: 64, Col: 40}
}
_, 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, 24, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
@@ -139,132 +304,18 @@ func weekDayComponent(user models.User, day models.WorkDay) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
templ_7745c5c3_Var14 := templ.GetChildren(ctx)
if templ_7745c5c3_Var14 == nil {
templ_7745c5c3_Var14 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
work, pause := day.GetWorkTimeString()
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"flex flex-row gap-2\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div class=\"flex flex-row gap-2\"><div class=\"flex flex-col\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = timeGaugeComponent(day.GetWorkDayProgress(user), false, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
if !day.RequiresAction() {
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"flex flex-col\"><p class=\"\"><span class=\"font-bold uppercase hidden md:inline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(day.Day.Format("Mon"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 39, Col: 89}
}
_, 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> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(day.Day.Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 39, Col: 130}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p><div class=\"flex flex-row gap-2\"><span class=\"text-accent\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(work)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 41, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span> <span class=\"text-neutral-500\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(pause)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 42, Col: 42}
}
_, 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, 16, "</span></div><div class=\"flex flex-row gap-2 items-center\"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" class=\"size-4\" viewBox=\"0 0 16 16\"><path d=\"M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z\"></path> <path d=\"M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0\"></path></svg> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if day.Absence.Datum.Equal(day.Day) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(day.Absence.AbwesenheitTyp.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 50, Col: 41}
}
_, 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, 18, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if !day.TimeFrom.Equal(day.TimeTo) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(day.TimeFrom.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 52, Col: 41}
}
_, 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, 20, "</span> <span>-</span> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(day.TimeTo.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 54, Col: 39}
}
_, 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, 21, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p>Keine Anwesenheit</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -272,7 +323,7 @@ func weekDayComponent(user models.User, day models.WorkDay) templ.Component {
})
}
func employeComponent(week models.WorkWeek) templ.Component {
func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -288,144 +339,292 @@ func employeComponent(week models.WorkWeek) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var14 := templ.GetChildren(ctx)
if templ_7745c5c3_Var14 == nil {
templ_7745c5c3_Var14 = templ.NopComponent
templ_7745c5c3_Var15 := templ.GetChildren(ctx)
if templ_7745c5c3_Var15 == nil {
templ_7745c5c3_Var15 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
year, kw := week.WeekStart.ISOWeek()
progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<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\"><p class=\"font-bold uppercase\">")
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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Vorname)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 70, Col: 53}
if !onlyAccept {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"lg:hidden\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = weekPicker(week.WeekStart).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<p class=\"font-bold uppercase\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Name)
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Vorname)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 70, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 93, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</p><div class=\"flex flex-row gap-2 col-span-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = timeGaugeComponent(uint8(progress), false, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<div><p>Arbeitszeit: ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDuration(week.Worktime)))
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 74, Col: 78}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 93, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</p><p>Überstunden: ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</p><div class=\"grid grid-cols-5 gap-2 lg:grid-cols-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !onlyAccept {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div class=\"col-span-2\"><span class=\"flex flex-row gap-2 items-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = statusCheckMark(week.CheckStatus(), models.WeekStatusSent).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "Gesendet</span> <span class=\"flex flex-row gap-2 items-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = statusCheckMark(week.CheckStatus(), models.WeekStatusAccepted).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Akzeptiert</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"flex flex-row gap-2 col-span-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = timeGaugeComponent(int8(progress), false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div><p>Arbeitszeit: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDuration(week.Overtime)))
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDuration(week.Worktime)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 75, Col: 79}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 110, Col: 79}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</p></div></div></div><div class=\"grid-cell col-span-3 flex flex-col @7xl:grid @7xl:grid-cols-5 gap-2 content-baseline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, day := range week.WorkDays {
templ_7745c5c3_Err = weekDayComponent(week.User, day).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div><form class=\"grid-cell flex flex-col justify-between gap-2\" method=\"post\"><p class=\"text-sm\"><span class=\"\">Woche:</span> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</p><p>Überstunden: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d-%d", kw, year))
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 85, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 111, Col: 90}
}
_, 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, 31, "</p><input type=\"hidden\" name=\"method\" value=\"accept\"> <input type=\"hidden\" name=\"user\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</p></div></div></div></div><div class=\"grid-cell col-span-3 flex flex-col @7xl:grid @7xl:grid-cols-5 gap-2 py-4 content-baseline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(week.User.PersonalNummer))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 87, Col: 82}
for _, day := range week.Days {
templ_7745c5c3_Err = defaultWeekDayComponent(week.User, day).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</div><div class=\"grid-cell flex flex-col gap-2 justify-between\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"> <input type=\"hidden\" name=\"week\" value=\"")
if onlyAccept {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<p class=\"text-sm\"><span class=\"\">Woche:</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d-%d", kw, year))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 123, Col: 86}
}
_, 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, 43, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"max-md:hidden\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = weekPicker(week.WeekStart).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<form method=\"post\" class=\"flex flex-col gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
week.CheckStatus()
method := "accept"
if !onlyAccept {
method = "send"
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<input type=\"hidden\" name=\"method\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(week.WeekStart.Format(time.DateOnly))
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 88, Col: 80}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 137, Col: 53}
}
_, 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, 33, "\"><div class=\"flex flex-col gap-2\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\"> <input type=\"hidden\" name=\"user\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if week.Status == models.WeekStatusDifferences {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<p class=\"text-red-600 text-sm\">Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen</p>")
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(week.User.PersonalNummer))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 138, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\"> <input type=\"hidden\" name=\"week\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(week.WeekStart.Format(time.DateOnly))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 139, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if onlyAccept {
if week.Status == models.WeekStatusDifferences {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<p class=\"text-red-600 text-sm\">Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " <button type=\"submit\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if week.Status == models.WeekStatusDifferences {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " class=\"btn\">Bestätigen</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
switch {
case week.RequiresAction():
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<p class=\"text-sm text-red-500\">bitte zuerst Buchungen anpassen</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case time.Since(week.WeekStart) < 24*7*time.Hour:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<p class=\"text-sm text-red-500\">Die Woche kann erst am nächsten Montag gesendet werden!</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case week.Status == models.WeekStatusNone:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<p class=\"text-sm\">an Vorgesetzten senden</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case week.Status == models.WeekStatusSent:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<p class=\"text-sm\">an Vorgesetzten gesendet</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case week.Status == models.WeekStatusAccepted:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<p class=\"text-sm\">vom Vorgesetzten bestätigt</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " <button")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if week.Status < models.WeekStatusSent {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " type=\"submit\" class=\"btn\">Korrigieren</button> <button")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if time.Since(week.WeekStart) < 24*7*time.Hour || week.Status >= models.WeekStatusSent || week.RequiresAction() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " type=\"submit\" class=\"btn\">Senden</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<button type=\"submit\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if week.Status == models.WeekStatusDifferences {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " class=\"btn\">Bestätigen</button></div></form></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</form></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -449,53 +648,53 @@ func userPresenceComponent(user models.User, present bool) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
if templ_7745c5c3_Var22 == nil {
templ_7745c5c3_Var22 = templ.NopComponent
templ_7745c5c3_Var24 := templ.GetChildren(ctx)
if templ_7745c5c3_Var24 == nil {
templ_7745c5c3_Var24 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"grid-cell group flex flex-row gap-2\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<div class=\"grid-cell group flex flex-row gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if present {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"h-8 bg-accent rounded-md group-hover:text-black md:text-transparent text-center p-1\">Anwesend</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<div class=\"h-8 bg-accent rounded-md group-hover:text-black md:text-transparent text-center p-1\">Anwesend</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<div class=\"h-8 bg-red-600 rounded-md group-hover:text-white md:text-transparent text-center p-1\">Abwesend</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<div class=\"h-8 bg-red-600 rounded-md group-hover:text-white md:text-transparent text-center p-1\">Abwesend</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname)
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 108, Col: 19}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 173, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
_, 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, 42, " ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name)
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 108, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 173, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
_, 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, 43, "</p></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@@ -1,113 +1,41 @@
package templates
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"fmt"
"net/url"
"strconv"
"time"
)
templ inputForm() {
{{
urlParams := ctx.Value("urlParams").(url.Values)
user := ctx.Value("user").(models.User)
}}
<div class="grid-sub divide-x-1 bg-neutral-300 max-md:flex max-md:flex-col">
<div class="grid-cell md:col-span-1 max-md:grid grid-cols-2">
<p class="font-bold uppercase">{ user.Vorname + " " + user.Name }</p>
<div class="justify-self-end">
<p class="text-sm">Überstunden</p>
<p class="text-accent">{ user.Overtime }</p>
</div>
</div>
<form id="timeRangeForm" method="GET" class="grid-cell flex flex-row md:col-span-3 gap-2 ">
@lineComponent()
<div class="flex flex-col gap-2 justify-between grow-1">
<input type="date" value={ urlParams.Get("time_from") } name="time_from" class="w-full bg-neutral-100 placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-0 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none focus:border-neutral-400 hover:border-neutral-300" placeholder="Zeitraum von..."/>
<input type="date" value={ urlParams.Get("time_to") } name="time_to" class="w-full bg-neutral-100 placeholder:text-neutral-400 text-neutral-700 text-sm border border-neutral-0 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none focus:border-neutral-400 hover:border-neutral-300" placeholder="Zeitraum bis..."/>
</div>
</form>
<div class="grid-cell content-end">
<button type="submit" form="timeRangeForm" class="btn bg-neutral-100 hover:bg-neutral-700 color-neutral-700">
<p class="">Anzeigen</p>
</button>
</div>
templ lineComponent() {
<div class="flex flex-col w-2 py-2 items-center text-accent print:hidden">
<svg class="size-2" viewBox="0 0 24 24" fill="currentColor">
<polygon points="12,2 22,12 12,22 2,12"></polygon>
</svg>
<div class="w-[2px] bg-accent flex-grow -my-1"></div>
<svg class="size-2" viewBox="0 0 24 24" fill="currentColor">
<polygon points="12,2 22,12 12,22 2,12"></polygon>
</svg>
</div>
}
templ dayComponent(workDay models.WorkDay, submitted bool) {
{{
work, pause := workDay.GetWorkTimeString()
user := ctx.Value("user").(models.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.GetWorkDayProgress(ctx.Value("user").(models.User)), workDay.Day.Equal(time.Now().Truncate(24*time.Hour)), workDay.RequiresAction())
<div>
<p class=""><span class="font-bold uppercase hidden md:inline">{ workDay.Day.Format("Mon") }:</span> { workDay.Day.Format("02.01.2006") }</p>
if work!="" {
<p class=" text-sm mt-1">Arbeitszeit:</p>
if (workDay.RequiresAction()) {
<p class="text-red-600">Bitte anpassen</p>
} else {
<p class=" text-accent">{ work }</p>
}
<p class="text-neutral-500 flex flex-row items-center"><span class="icon-[material-symbols-light--motion-photos-paused-outline]"></span>{ pause }</p>
if overtime != "" {
<p class="text-neutral-500 flex flex-row items-center"><span class="icon-[material-symbols-light--more-time]"></span>{ 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 (workDay.Absence != models.Absence{}) {
<p>{ workDay.Absence.AbwesenheitTyp.Name }</p>
}
if len(workDay.Bookings) < 1 && (workDay.Absence == models.Absence{}) {
<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)
}
@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) {
<button class="btn w-auto group" type="submit" onclick={ templ.JSFuncCall("editDay", 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>
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) }>
<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]: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="group-[.edit]/button:hidden md:hidden" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"></path>
<path class="hidden group-[.edit]/button:block md:hidden" d="M12.736 3.97a.733.733 0 0 1 j1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425z"></path>
</svg>
</button>
<button class="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 uint8, today bool, warning bool) {
templ timeGaugeComponent(progress int8, today bool) {
{{
var bgColor string
switch {
case (warning):
case (0 > progress):
bgColor = "bg-red-600"
break
case (progress > 0 && progress < 95):
@@ -133,49 +61,68 @@ templ timeGaugeComponent(progress uint8, today bool, warning bool) {
}
}
templ lineComponent() {
<div class="flex flex-col w-2 py-2 items-center text-accent print:hidden">
<svg class="size-2" viewBox="0 0 24 24" fill="currentColor">
<polygon points="12,2 22,12 12,22 2,12"></polygon>
</svg>
<div class="w-[2px] bg-accent flex-grow -my-1"></div>
<svg class="size-2" viewBox="0 0 24 24" fill="currentColor">
<polygon points="12,2 22,12 12,22 2,12"></polygon>
</svg>
templ newAbsenceComponent() {
<div class="no-booking-component hidden group-[.edit]:flex flex-col gap-2 align-center ">
<button type="button" name="absence" onclick={ templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), 0, false) } class="btn border-neutral-500">
Neue Abwesenheit
</button>
</div>
}
templ absenceComponent(d models.WorkDay) {
<div class="no-booking-component hidden group-[.edit]:flex flex-col gap-2 align-center">
<select name="absence" onchange={ templ.JSFuncCall("editAbwesenheit", templ.JSExpression("this"), templ.JSExpression("event")) } class="grow cursor-pointer rounded-md text-neutral-800 p-2 md:px-4 border text-center text-sm transition-colors border-neutral-900" disabled>
<option value="0">Abwesenheit?</option>
for _, absence := range models.GetAbsenceTypesCached() {
<option value={ strconv.Itoa(int(absence.Id)) }>{ absence.Name }</option>
templ absenceComponent(a *models.Absence, isKurzarbeit bool) {
{{
editBox := ""
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_id" value={ a.CounterId }/>
<p class="whitespace-nowrap group-[.edit]:ml-2">
{ a.AbwesenheitTyp.Name }
if a.IsMultiDay() {
<span class="text-neutral-500">bis { a.DateTo.Format("02.01.2006") }</span>
}
</select>
</p>
<div class="w-full"></div>
if isKurzarbeit {
<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>
}
templ newBookingComponent(d models.WorkDay) {
<div class="new-booking-component hidden group-[.edit]:flex flex-row gap-2 items-center">
<button name="action" value="add" type="submit" class="hover:text-accent cursor-pointer icon-[material-symbols-light--add-circle-outline]"></button>
<input name="timestamp" type="time" value={ time.Now().Format("15:04") } class="text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm border border-neutral-200 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none focus:border-neutral-400 hover:border-neutral-300"/>
<input name="date" type="hidden" value={ d.Day.Format("2006-01-02") }/>
<select name="check_in_out">
<option value="0" disabled>Kommen/Gehen</option>
<option value="3" selected?={ len(d.Bookings) > 0 && d.Bookings[len(d.Bookings)-1].CheckInOut%2 == 0 }>Kommen</option>
<option value="4" selected?={ len(d.Bookings) > 0 && d.Bookings[len(d.Bookings)-1].CheckInOut%2 == 1 }>Gehen</option>
</select>
templ newBookingComponent(d *models.WorkDay) {
<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="date" type="hidden" value={ d.Day.Format(time.DateOnly) }/>
<div class="relative">
<select class="cursor-pointer appearance-none" name="check_in_out">
<option value="0" disabled>Kommen/Gehen</option>
<option value="3" selected?={ len(d.Bookings) > 0 && d.Bookings[len(d.Bookings)-1].CheckInOut%2 == 0 }>Kommen</option>
<option value="4" selected?={ len(d.Bookings) > 0 && d.Bookings[len(d.Bookings)-1].CheckInOut%2 == 1 }>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 bookingComponent(booking models.Booking) {
<div>
<p class="text-neutral-500">
<span class="text-neutral-700 group-[.edit]:hidden inline">{ booking.Timestamp.Format("15:04") }</span>
<input disabled name={ "booking_" + strconv.Itoa(booking.CounterId) } type="time" value={ booking.Timestamp.Format("15:04") } class="text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm border border-neutral-200 rounded-md px-3 py-2 transition duration-300 ease focus:outline-none focus:border-neutral-400 hover:border-neutral-300"/>
<p class="text-neutral-500 edit-box">
<span class="text-black group-[.edit]:hidden inline">{ booking.Timestamp.Format("15:04") }</span>
<input disabled name={ "booking_" + strconv.Itoa(booking.CounterId) } type="time" value={ booking.Timestamp.Format("15:04") } class="text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer"/>
{ booking.GetBookingType() }
</p>
if booking.IsSubmittedAndChecked() {
<p>submitted</p>
}
</div>
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
package templates
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"net/url"
"strconv"
"time"
)
templ TimePage(workDays []models.WorkDay, lastSub time.Time) {
{{
allDays := ctx.Value("days").([]models.IWorkDay)
}}
@Base()
@headerComponent()
<div class="grid-main divide-y-1">
@inputForm()
for _, day := range allDays {
@defaultDayComponent(day)
if (day.Date().Weekday() == time.Monday) {
<div class="grid-sub responsive bg-neutral-300 h-2"></div>
}
}
</div>
@LegendComponent()
}
templ inputForm() {
{{
urlParams := ctx.Value("urlParams").(url.Values)
user := ctx.Value("user").(models.User)
}}
<div class="grid-sub divide-x-1 bg-neutral-300 responsive">
<div class="grid-cell md:col-span-1 max-md:grid grid-cols-2">
<p class="font-bold uppercase">{ user.Vorname + " " + user.Name }</p>
<div class="justify-self-end">
<p class="text-sm">Überstunden</p>
<p class="text-accent">{ user.Overtime }</p>
</div>
</div>
<form id="timeRangeForm" method="GET" class="grid-cell flex flex-row md:col-span-3 gap-2 ">
@lineComponent()
<div class="flex flex-col gap-2 justify-between grow-1">
<input type="date" value={ urlParams.Get("time_from") } name="time_from" class="btn bg-neutral-100" placeholder="Zeitraum von..."/>
<input type="date" value={ urlParams.Get("time_to") } name="time_to" class="btn bg-neutral-100" placeholder="Zeitraum bis..."/>
</div>
</form>
<div class="grid-cell content-end">
<button type="submit" form="timeRangeForm" class="btn bg-neutral-100 hover:bg-neutral-700 color-neutral-700">
<p class="">Anzeigen</p>
</button>
</div>
</div>
<form id="absence_form" method="POST" action="/absence" class="grid-sub responsive scroll-m-2 bg-neutral-300 hidden">
<input type="hidden" name="aw_id" value=""/>
<div class="grid-cell border-r-1"><p class="font-bold uppercase">Abwesenheit</p></div>
<div class="grid-cell">
<label class="block mb-1 text-sm text-neutral-700">Abwesenheitsart</label>
<div class="relative">
<select name="aw_type" class="btn appearance-none cursor-pointer bg-neutral-100">
for _, absence := range models.GetAbsenceTypesCached() {
<option value={ strconv.Itoa(int(absence.Id)) }>{ absence.Name }</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 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>
}
templ defaultDayComponent(day models.IWorkDay) {
{{
user := ctx.Value("user").(models.User)
justify := "justify-center"
if day.IsWorkDay() && len(day.(*models.WorkDay).Bookings) > 1 {
justify = "justify-between"
}
}}
<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">
@timeGaugeComponent(day.GetDayProgress(user), day.Date().Equal(time.Now().Truncate(24*time.Hour)))
<div>
<p>
<span class="font-bold uppercase hidden md:inline">{ helper.FormatGermanDayOfWeek(day.Date()) }:</span> { day.Date().Format("02.01.2006") }
</p>
if day.IsWorkDay() {
{{
workDay, _ := day.(*models.WorkDay)
work, pause, overtime := workDay.GetAllWorkTimesReal(user)
}}
if day.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--schedule-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 && len(workDay.Bookings) > 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 grid-cell flex flex-row md:col-span-3 col-span-2 gap-2 w-full">
@lineComponent()
<form id={ "time-" + day.Date().Format(time.DateOnly) } class={ "flex flex-col gap-2 w-full", justify } method="post">
if day.IsWorkDay() {
{{
workDay, _ := day.(*models.WorkDay)
}}
@newAbsenceComponent()
if len(workDay.Bookings) < 1 {
<p class="text group-[.edit]:hidden">Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!</p>
}
if workDay.IsKurzArbeit() {
@absenceComponent(workDay.GetKurzArbeit(), true)
}
for _, booking := range workDay.Bookings {
@bookingComponent(booking)
}
@newBookingComponent(workDay)
} else {
{{
absentDay, _ := day.(*models.Absence)
}}
@absenceComponent(absentDay, false)
}
<input type="hidden" name="action" value="change"/> <!-- default action value for ändern button -->
</form>
</div>
<div class="grid-cell flex flex-row gap-2 items-end">
@changeButtonComponent("time-"+day.Date().Format(time.DateOnly), day.IsWorkDay())
</div>
</div>
}
templ absentInput(a models.Absence) {
<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_id" value={ a.CounterId }/>
}

View File

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

33
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";
CREATE TABLE "s_anwesenheit_typen" (
"anwesenheit_id" int2 PRIMARY KEY,
"anwesenheit_name" varchar(255)
"anwesenheit_name" varchar(255) NOT NULL
);
-- ----------------------------
@@ -40,11 +40,11 @@ CREATE TABLE "s_personal_daten" (
"geschlecht" int2,
"card_uid" varchar(255),
"hauptbeschaeftigungs_ort" int2,
"arbeitszeit_per_tag" float4 NOT NULL,
"arbeitszeit_per_woche" float4 NOT NULL,
"arbeitszeit_per_tag" float4,
"arbeitszeit_per_woche" float4,
"arbeitszeit_min_start" time(6),
"arbeitszeit_max_ende" time(6),
"vorgesetzter_pers_nr" int4 NOT NULL
"vorgesetzter_pers_nr" int4
);
COMMENT ON COLUMN "s_personal_daten"."geschlecht" IS '1==weiblich, 2==maennlich, 3==divers';
@@ -78,29 +78,34 @@ EXECUTE FUNCTION update_zuletzt_geandert();
DROP TABLE IF EXISTS "wochen_report";
CREATE TABLE "wochen_report" (
"id" serial PRIMARY KEY,
"personal_nummer" int4,
"woche_start" date,
"personal_nummer" int4 NOT NULL,
"woche_start" date NOT NULL,
"bestaetigt" bool DEFAULT FALSE,
"arbeitszeit" interval,
"ueberstunden" interval,
"arbeitszeit" interval NOT NULL,
"ueberstunden" interval NOT NULL,
"anwesenheiten" int ARRAY,
"abwesenheiten" int ARRAY,
UNIQUE ("personal_nummer", "woche_start")
);
DROP TABLE IF EXISTS "abwesenheit";
CREATE TABLE "abwesenheit" (
"counter_id" bigserial PRIMARY KEY,
"card_uid" varchar(255),
"abwesenheit_typ" int2,
"datum" timestamptz(6) DEFAULT NOW()::DATE
"card_uid" varchar(255) NOT NULL,
"abwesenheit_typ" int2 NOT NULL,
"datum_from" timestamptz DEFAULT NOW()::DATE NOT NULL,
"datum_to" timestamptz NOT NULL
);
DROP TABLE IF EXISTS "s_abwesenheit_typen";
CREATE TABLE "s_abwesenheit_typen" (
"abwesenheit_id" int2 PRIMARY KEY,
"abwesenheit_name" varchar(255),
"arbeitszeit_equivalent" float4
"abwesenheit_id" int2 PRIMARY KEY NOT NULL,
"abwesenheit_name" varchar(255) NOT NULL,
"arbeitszeit_equivalent" float4 NOT NULL
);
COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; 1=Arbeitszeit auffüllen; 2=Arbeitszeit austauschen';
-- Adds crypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;

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

@@ -1,5 +1,5 @@
INSERT INTO "s_personal_daten" ("personal_nummer", "aktiv_beschaeftigt", "vorname", "nachname", "geburtsdatum", "plz", "adresse", "geschlecht", "card_uid", "hauptbeschaeftigungs_ort", "arbeitszeit_per_tag", "arbeitszeit_min_start", "arbeitszeit_max_ende", "vorgesetzter_pers_nr") VALUES
(123, 't', 'Kim', 'Mustermensch', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'aaaa-aaaa', 1, 8, '07:00:00', '20:00:00', 0);
INSERT INTO "s_personal_daten" ("personal_nummer", "aktiv_beschaeftigt", "vorname", "nachname", "geburtsdatum", "plz", "adresse", "geschlecht", "card_uid", "hauptbeschaeftigungs_ort", "arbeitszeit_per_tag", "arbeitszeit_per_woche", "arbeitszeit_min_start", "arbeitszeit_max_ende", "vorgesetzter_pers_nr") VALUES
(123, 't', 'Kim', 'Mustermensch', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'aaaa-aaaa', 1, 8, 40, '07:00:00', '20:00:00', 0);
INSERT INTO "user_password" ("personal_nummer", "pass_hash") VALUES
(123, crypt('max_pass', gen_salt('bf')));

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
services:
db:
image: postgres:16
restart: unless-stopped
env_file:
- .env
environment:
PGDATA: /var/lib/postgresql/data/pg_data
volumes:
- ${POSTGRES_PATH}:/var/lib/postgresql/data
# - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
@@ -19,19 +13,9 @@ services:
ports:
- 8001:8080
backend:
image: git.letsstein.de/tom/arbeitszeitmessung
restart: unless-stopped
env_file:
- .env
environment:
POSTGRES_HOST: db
POSTGRES_DB: ${POSTGRES_DB}
EXPOSED_PORT: ${EXPOSED_PORT}
NO_CORS: true
ports:
- ${EXPOSED_PORT}:8080
depends_on:
- db
swagger:
image: swaggerapi/swagger-ui
restart: unless-stopped

View File

@@ -6,26 +6,31 @@ services:
env_file:
- .env
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
PGTZ: ${TZ}
PGDATA: /var/lib/postgresql/data/pg_data
volumes:
- ${POSTGRES_PATH}:/var/lib/postgresql/data
- ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
ports:
- 5432:5432
- ${POSTGRES_PORT}:5432
backend:
image: git.letsstein.de/tom/arbeitszeitmessung
image: git.letsstein.de/tom/arbeitszeitmessung-webserver
env_file:
- .env
environment:
POSTGRES_HOST: db
POSTGRES_DB: ${POSTGRES_DB}
EXPOSED_PORT: ${EXPOSED_PORT}
ports:
- ${EXPOSED_PORT}:8080
- ${WEB_PORT}:8080
depends_on:
- db
- document-creator
volumes:
- ../logs:/app/Backend/logs
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,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

@@ -45,10 +45,18 @@ generateFrontend:
backend: generateFrontend login_registry
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:latest Backend --push
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:${GIT_COMMIT} Backend --push
# docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:${GIT_COMMIT} Backend //--push
test:
$(MAKE) -C Backend test
scan: test
$(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
[![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
## Installation

63
WIR-typst/main.typ Normal file
View File

@@ -0,0 +1,63 @@
#set page("a4")
#set text(font: "Lato")
= Stunden
== Kim Mustermensch
Zeitraum: 01.10.2025 - 31.10.2025
Arbeitszeit: 136h 19min
Überstunden: -39h 41min
// #show table.cell: it => {
// if it.y == 0 {
// set text(white)
// strong(it)
// } else if it.body == [] {
// // Replace empty cells with 'N/A'
// pad(..it.inset)[0min]
// } else {
// it
// }
// }
#let subgrid(body) = {
table.cell(colspan: 3, inset: 0em)[
#table(
columns: (1fr, 1fr, 1fr),
gutter: 0em,
stroke: black,
[..#body]
)
]
}
"01.09.2025",
"08:07",
"16:28",
"Büro",
"7h 51min",
"30min",
"-9min",
"02.09.2025",
// return work, pause, overtime
table.cell(colspan: 3, inset: 0em)[#table(
columns: (1fr, 1fr, 1fr),
gutter: 0em,
stroke: black,
[08:12], [16:24], [Büro],
[16:30], [17:24], [Homeoffice]
)],
"6h",
"0min",
"-1h 15min"
)

BIN
WIR-typst/template.pdf Normal file

Binary file not shown.

36
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,
1 AS anwesenheit_typ
FROM days d
),
ins_anw AS (
-- insert only bookings up to now (prevents future times on today)
INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id)
SELECT ts, card_uid, check_in_out, geraet_id
FROM sample_bookings
WHERE ts <= NOW()
RETURNING 1
)
-- insert only bookings up to now (prevents future times on today)
INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id)
SELECT ts, card_uid, check_in_out, geraet_id
FROM sample_bookings
WHERE ts <= NOW()
RETURNING 1;
-- now insert absences (uses the same days CTE)
INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum)
SELECT
@@ -198,12 +198,12 @@ ORDER BY d.work_date;
WITH params AS (
SELECT
'acde-edca'::varchar AS card_uid,
'aaaa-aaaa'::varchar AS card_uid,
101::int AS geraet_id,
14::int AS start_days_ago, -- how many days back to start
0::int AS end_days_ahead, -- how many days forward (0 = today)
0.5::float AS pause_probability,
0.2::float AS absence_probability
0::float AS pause_probability,
0.0::float AS absence_probability
),
days AS (
SELECT gs::date AS work_date, p.card_uid, p.geraet_id, p.pause_probability, p.absence_probability
@@ -247,15 +247,13 @@ all_bookings AS (
SELECT * FROM base_bookings
UNION ALL
SELECT * FROM pause_bookings
),
ins_anw AS (
INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id)
SELECT ts, card_uid, check_in_out, geraet_id
FROM all_bookings
WHERE ts <= NOW()
ORDER BY work_date, ts
RETURNING 1
)
INSERT INTO anwesenheit ("timestamp", "card_uid", "check_in_out", "geraet_id", "anwesenheit_typ")
SELECT ts, card_uid, check_in_out, geraet_id, 1 as anwesenheit_typ
FROM all_bookings
WHERE ts <= NOW()
ORDER BY work_date, ts;
INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum)
SELECT
d.card_uid,

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

@@ -1,4 +1,4 @@
-- reverse: modify "s_personal_daten" table
ALTER TABLE "s_personal_daten" ALTER COLUMN "arbeitszeit_per_woche" DROP NOT NULL, ALTER COLUMN "vorgesetzter_pers_nr" DROP NOT NULL, ALTER COLUMN "arbeitszeit_per_tag" DROP NOT NULL, ALTER COLUMN "nachname" DROP NOT NULL, ALTER COLUMN "vorname" DROP NOT NULL;
ALTER TABLE "s_personal_daten" ALTER COLUMN "nachname" DROP NOT NULL, ALTER COLUMN "vorname" DROP NOT NULL;
-- reverse: modify "anwesenheit" table
ALTER TABLE "anwesenheit" ALTER COLUMN "anwesenheit_typ" DROP NOT NULL, ALTER COLUMN "geraet_id" DROP NOT NULL, ALTER COLUMN "timestamp" DROP NOT NULL;

View File

@@ -1,4 +1,4 @@
-- modify "anwesenheit" table
ALTER TABLE "anwesenheit" ALTER COLUMN "timestamp" SET NOT NULL, ALTER COLUMN "geraet_id" SET NOT NULL, ALTER COLUMN "anwesenheit_typ" SET NOT NULL;
-- modify "s_personal_daten" table
ALTER TABLE "s_personal_daten" ALTER COLUMN "vorname" SET NOT NULL, ALTER COLUMN "nachname" SET NOT NULL, ALTER COLUMN "arbeitszeit_per_tag" SET NOT NULL, ALTER COLUMN "vorgesetzter_pers_nr" SET NOT NULL, ALTER COLUMN "arbeitszeit_per_woche" SET NOT NULL;
ALTER TABLE "s_personal_daten" ALTER COLUMN "vorname" SET NOT NULL, ALTER COLUMN "nachname" SET NOT NULL;

View File

@@ -1,13 +1,19 @@
ALTER TABLE wochen_report
ALTER COLUMN ueberstunden TYPE interval
USING
make_interval(
hours => floor(ueberstunden)::int,
mins => round((ueberstunden - floor(ueberstunden)) * 60)::int
),
ALTER COLUMN arbeitszeit TYPE interval
USING
make_interval(
hours => floor(arbeitszeit)::int,
mins => round((arbeitszeit - floor(arbeitszeit)) * 60)::int
);
ADD COLUMN ueberstunden_interval interval,
ADD COLUMN arbeitszeit_interval interval;
UPDATE wochen_report
SET
ueberstunden_interval = CASE WHEN ueberstunden IS NULL THEN NULL ELSE (ueberstunden::double precision * INTERVAL '1 hour') END,
arbeitszeit_interval = CASE WHEN arbeitszeit IS NULL THEN NULL ELSE (arbeitszeit::double precision * INTERVAL '1 hour') END;
-- when happy, drop old columns and rename new ones
ALTER TABLE wochen_report
DROP COLUMN ueberstunden,
DROP COLUMN arbeitszeit;
ALTER TABLE wochen_report
RENAME COLUMN ueberstunden_interval TO ueberstunden;
ALTER TABLE wochen_report
RENAME COLUMN arbeitszeit_interval TO arbeitszeit;

View File

@@ -0,0 +1,11 @@
-- reverse: modify "wochen_report" table
ALTER TABLE "wochen_report" ALTER COLUMN "personal_nummer" DROP NOT NULL;
-- reverse: modify "s_personal_daten" table
ALTER TABLE "s_anwesenheit_typen" ALTER COLUMN "anwesenheit_name" DROP NOT NULL;
-- reverse: set comment to column: "arbeitszeit_equivalent" on table: "s_abwesenheit_typen"
COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS NULL;
-- reverse: modify "s_abwesenheit_typen" table
ALTER TABLE "s_abwesenheit_typen" ALTER COLUMN "arbeitszeit_equivalent" DROP NOT NULL, ALTER COLUMN "abwesenheit_name" DROP NOT NULL;
-- reverse: modify "abwesenheit" table
ALTER TABLE "abwesenheit" DROP COLUMN "datum_to", ALTER COLUMN "datum_from" DROP NOT NULL, ALTER COLUMN "abwesenheit_typ" DROP NOT NULL, ALTER COLUMN "card_uid" DROP NOT NULL;
ALTER TABLE "abwesenheit" RENAME COLUMN "datum_from" TO "datum";

View File

@@ -0,0 +1,11 @@
-- modify "abwesenheit" table
ALTER TABLE "abwesenheit" RENAME COLUMN "datum" TO "datum_from";
ALTER TABLE "abwesenheit" ALTER COLUMN "card_uid" SET NOT NULL, ALTER COLUMN "abwesenheit_typ" SET NOT NULL, ALTER COLUMN "datum_from" SET NOT NULL, ADD COLUMN "datum_to" timestamptz;
-- modify "s_abwesenheit_typen" table
ALTER TABLE "s_abwesenheit_typen" ALTER COLUMN "abwesenheit_name" SET NOT NULL, ALTER COLUMN "arbeitszeit_equivalent" SET NOT NULL;
-- set comment to column: "arbeitszeit_equivalent" on table: "s_abwesenheit_typen"
COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; 1=Arbeitszeit auffüllen; 2=Arbeitszeit austauschen';
-- modify "s_anwesenheit_typen" table
ALTER TABLE "s_anwesenheit_typen" ALTER COLUMN "anwesenheit_name" SET NOT NULL;
-- modify "s_personal_daten" table
ALTER TABLE "wochen_report" ALTER COLUMN "personal_nummer" SET NOT NULL;

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,7 +1,9 @@
h1:31y/DB0ca2cm603nNckNvVCwm+XuMaVsoGUB+JvrOKs=
h1:gE7ikkZS7bQbedAjVspQKftSo5ODij2eiQGWbfQEYmI=
20250901201159_initial.up.sql h1:Mb1RlVdFvcxqU9HrSK6oNeURqFa3O4KzB3rDa+6+3gc=
20250901201250_control_tables.up.sql h1:a5LATgR/CRiC4GsqxkJ94TyJOxeTcW74eCnodIy+c1E=
20250901201710_triggers_extension.up.sql h1:z9b6Hk9btE2Ns4mU7B16HjvYBP6EEwHAXVlvPpkn978=
20250903221313_overtime.up.sql h1:t/B435ShW5ZEnzC81jRABWVZ5gNm7tPZPnOO6/ZY6ow=
20250903233030_non_null_contraints.up.sql h1:e7d+6ZdpEjPh2cc65N3S06oD2e6diMuG7+klhgYsym8=
20250904114004_intervals.up.sql h1:Sz8FIVvvcCIS3aIuSjyzFYLs32fjMcMMHk62shj6Qpw=
20250903233030_non_null_contraints.up.sql h1:YKeYgazfh+jPyh7hFT/pV+By8eHnk1taXnlgSLyXSA0=
20250904114004_intervals.up.sql h1:gDdN8cJ4xH1vQhAbbhqD5lwdyEO1N9EIqEYkmWGiWIU=
20250916093608_kurzarbeit.up.sql h1:yDAAMLyUXz6b7+MI6XK/HZMPzutKoT2NNNOCjFaqSts=
20251013212224_buchungs_array.up.sql h1:mbhvnwMUkEFFQQ41NC47auqxbtvNkztziWvpLDFm6tA=

19
package-lock.json generated
View File

@@ -10,7 +10,8 @@
},
"devDependencies": {
"@iconify-json/material-symbols-light": "^1.2.33",
"@iconify/tailwind4": "^1.0.6"
"@iconify/tailwind4": "^1.0.6",
"prettier": "^3.6.2"
}
},
"node_modules/@antfu/install-pkg": {
@@ -1294,6 +1295,22 @@
"pathe": "^2.0.3"
}
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/quansync": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",

View File

@@ -5,6 +5,7 @@
},
"devDependencies": {
"@iconify-json/material-symbols-light": "^1.2.33",
"@iconify/tailwind4": "^1.0.6"
"@iconify/tailwind4": "^1.0.6",
"prettier": "^3.6.2"
}
}