55 Commits
0.2.0 ... 1.1.2

Author SHA1 Message Date
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
66db633dc6 Merge pull request 'dev/ui' (#33) from dev/ui into main
All checks were successful
GoLang Tests / Run Go Tests (push) Successful in 27s
Reviewed-on: #33
2025-09-04 22:12:52 +02:00
fe442e8eef closes #14
All checks were successful
GoLang Tests / Run Go Tests (push) Successful in 33s
2025-09-04 22:07:54 +02:00
9ded540314 closed #25, #32
All checks were successful
GoLang Tests / Run Go Tests (push) Successful in 49s
2025-09-04 21:22:26 +02:00
0dd75c2126 Update build-deploy.yaml
All checks were successful
GoLang Tests / Run Go Tests (push) Successful in 59s
2025-09-04 10:23:37 +02:00
327e47840b Fixed tests and git actions (#30)
All checks were successful
GoLang Tests / Run Go Tests (push) Successful in 2m46s
Arbeitszeitmessung Deploy / Build Go Image and Upload (push) Successful in 2m58s
Reviewed-on: #30
Co-authored-by: Tom Tröger <t.troeger.02@gmail.com>
Co-committed-by: Tom Tröger <t.troeger.02@gmail.com>
2025-09-04 10:16:42 +02:00
e9f8ab0a56 Merge pull request 'dev/main -- added License' (#29) from dev/main into main
Some checks failed
GoLang Tests / Run Go Tests (push) Failing after 48s
GoLang Tests / Build Go Image and Upload (push) Has been skipped
Reviewed-on: #29
2025-09-04 09:24:29 +02:00
36884f4d96 Merge pull request 'dev/actions' (#26) from dev/actions into dev/main
Some checks failed
GoLang Tests / Run Go Tests (push) Failing after 41s
GoLang Tests / Build Go Image and Upload (push) Has been skipped
Reviewed-on: #26
2025-08-29 15:36:00 +02:00
fd2c702b5f Add LICENSE
Some checks failed
GoLang Tests / Run Go Tests (push) Failing after 54s
GoLang Tests / Build Go Image and Upload (push) Has been skipped
2025-08-28 10:55:46 +02:00
61 changed files with 4452 additions and 1844 deletions

View File

@@ -1,6 +1,8 @@
name: GoLang Tests name: Arbeitszeitmessung Deploy
run-name: ${{ gitea.actor }} is testing golang Code run-name: ${{ gitea.actor }} is building and deploying arbeitszeitmesssung
on: [push] on:
push:
tags: "*"
jobs: jobs:
testing: testing:
@@ -47,9 +49,9 @@ jobs:
- name: Run Go Tests - name: Run Go Tests
run: cd Backend && go test ./... run: cd Backend && go test ./...
build: build:
needs: testing
name: Build Go Image and Upload name: Build Go Image and Upload
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [testing]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -68,5 +70,7 @@ jobs:
with: with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: git.letsstein.de/tom/arbeitszeitmessung:latest
context: Backend 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_USER: root
POSTGRES_PASSWORD: password
POSTGRES_DB: arbeitszeitmessung
POSTGRES_PORT: 5432
RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache
steps:
- name: Checkout
uses: actions/checkout@v4
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

@@ -38,3 +38,4 @@ DB/pg_data
node_modules node_modules
atlas.hcl atlas.hcl
.scannerwork .scannerwork
Backend/logs

View File

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

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

@@ -0,0 +1,41 @@
package endpoints
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"arbeitszeitmessung/templates"
"log"
"net/http"
"time"
)
func PDFHandler(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r)
startDate, err := parseTimestamp(r, "start", time.Now().Format("2006-01-02"))
if err != nil {
log.Println("Error parsing 'start_date' time", err)
http.Error(w, "Timestamp 'start_date' cannot be parsed!", http.StatusBadRequest)
return
}
if startDate.Day() > 1 {
startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1))
}
endDate := startDate.AddDate(0, 1, -1)
user, err := models.GetUserFromSession(Session, r.Context())
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)
}
// log.Printf("Using Dates: %s - %s\n", startDate.String(), endDate.String())
templates.PDFReportEmploye(user, aggregatedOvertime, aggregatedWorkTime, weeks, startDate, endDate).Render(r.Context(), w)
}

View File

@@ -56,7 +56,7 @@ func submitReport(w http.ResponseWriter, r *http.Request) {
} }
func showWeeks(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 { if err != nil {
log.Println("No user found with the given personal number!") log.Println("No user found with the given personal number!")
http.Redirect(w, r, "/user/login", http.StatusSeeOther) http.Redirect(w, r, "/user/login", http.StatusSeeOther)

View File

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

View File

@@ -5,6 +5,7 @@ import (
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"arbeitszeitmessung/templates" "arbeitszeitmessung/templates"
"context" "context"
"database/sql"
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
@@ -30,6 +31,22 @@ 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) { func parseTimestamp(r *http.Request, getKey string, fallback string) (time.Time, error) {
getTimestamp := r.URL.Query().Get(getKey) getTimestamp := r.URL.Query().Get(getKey)
if getTimestamp == "" { if getTimestamp == "" {
@@ -44,7 +61,7 @@ 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 // Returns bookings from DB with similar card uid -> checks for card uid in http query params
func getBookings(w http.ResponseWriter, r *http.Request) { 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 { if err != nil {
log.Println("No user found with the given personal number!") log.Println("No user found with the given personal number!")
http.Redirect(w, r, "/user/login", http.StatusSeeOther) http.Redirect(w, r, "/user/login", http.StatusSeeOther)
@@ -66,18 +83,18 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
} }
tsTo = tsTo.AddDate(0, 0, 1) // so that today is inside tsTo = tsTo.AddDate(0, 0, 1) // so that today is inside
workDays := (*models.WorkDay).GetWorkDays(nil, user.CardUID, tsFrom, tsTo) days := models.GetDays(user, tsFrom, tsTo, true)
sort.Slice(workDays, func(i, j int) bool { sort.Slice(days, func(i, j int) bool {
return workDays[i].Day.After(workDays[j].Day) return days[i].Date().After(days[j].Date())
}) })
lastSub := user.GetLastWorkWeekSubmission() lastSub := user.GetLastWorkWeekSubmission()
var aggregatedOvertime time.Duration var aggregatedOvertime time.Duration
for _, days := range workDays { for _, day := range days {
if days.Day.Before(lastSub) { if day.Date().Before(lastSub) {
continue continue
} }
aggregatedOvertime += days.CalcOvertime(user) aggregatedOvertime += day.TimeOvertimeReal(user)
} }
if reportedOvertime, err := user.GetReportedOvertime(); err == nil { if reportedOvertime, err := user.GetReportedOvertime(); err == nil {
user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute) user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute)
@@ -88,16 +105,13 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Accept") == "application/json" { if r.Header.Get("Accept") == "application/json" {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(workDays) json.NewEncoder(w).Encode(days)
return return
} }
if err != nil {
log.Println("Error calc overtime: ", err)
}
ctx := context.WithValue(r.Context(), "user", user) 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) { func updateBooking(w http.ResponseWriter, r *http.Request) {
@@ -108,11 +122,12 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
log.Println("Error loading location", err) log.Println("Error loading location", err)
loc = time.Local loc = time.Local
} }
user, err := (*models.User).GetUserFromSession(nil, Session, r.Context()) user, err := models.GetUserFromSession(Session, r.Context())
if err != nil { if err != nil {
log.Println("No user found!", err) log.Println("No user found!", err)
return return
} }
switch r.FormValue("action") { switch r.FormValue("action") {
case "add": case "add":
timestamp, err := time.ParseInLocation("2006-01-02|15:04", r.FormValue("date")+"|"+r.FormValue("timestamp"), loc) timestamp, err := time.ParseInLocation("2006-01-02|15:04", r.FormValue("date")+"|"+r.FormValue("timestamp"), loc)
@@ -135,14 +150,14 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
log.Printf("Error inserting booking %v -> %v\n", newBooking, err) log.Printf("Error inserting booking %v -> %v\n", newBooking, err)
} }
case "change": case "change":
absenceType, err := strconv.Atoi(r.FormValue("absence")) // absenceType, err := strconv.Atoi(r.FormValue("absence"))
if err != nil { // if err != nil {
log.Println("Error parsing absence type.", err) // log.Println("Error parsing absence type.", err)
absenceType = 0 // absenceType = 0
} // }
if absenceType != 0 { // if absenceType != 0 {
createAbsence(absenceType, user, loc, r) // createAbsence(absenceType, user, loc, r)
} // }
for index, possibleBooking := range r.PostForm { for index, possibleBooking := range r.PostForm {
if len(index) > 7 && index[:7] == "booking" { if len(index) > 7 && index[:7] == "booking" {
booking_id, err := strconv.Atoi(index[8:]) booking_id, err := strconv.Atoi(index[8:])
@@ -160,14 +175,93 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
log.Println("Error parsing time!", err) log.Println("Error parsing time!", err)
continue continue
} }
// log.Println("Parsing time", parsedTime)
booking.UpdateTime(parsedTime) booking.UpdateTime(parsedTime)
} }
} }
default:
log.Println("No action from /time found")
} }
getBookings(w, r) getBookings(w, r)
} }
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("Error loading location", err)
loc = time.Local
}
dateFrom, err := time.ParseInLocation("2006-01-02", r.FormValue("date_from"), loc)
if err != nil {
log.Println("Error parsing date_from input for absence", err)
return err
}
dateTo, err := time.ParseInLocation("2006-01-02", r.FormValue("date_to"), loc)
if err != nil {
log.Println("Error parsing date_to input for absence", err)
return err
}
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
}
func createAbsence(absenceType int, user models.User, loc *time.Location, r *http.Request) { func createAbsence(absenceType int, user models.User, loc *time.Location, r *http.Request) {
absenceDate, err := time.ParseInLocation("2006-01-02", r.FormValue("date"), loc) absenceDate, err := time.ParseInLocation("2006-01-02", r.FormValue("date"), loc)
if err != nil { if err != nil {
@@ -186,82 +280,3 @@ func createAbsence(absenceType int, user models.User, loc *time.Location, r *htt
return return
} }
} }
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)
}

View File

@@ -1,7 +1,6 @@
package endpoints package endpoints
import ( import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"arbeitszeitmessung/templates" "arbeitszeitmessung/templates"
"context" "context"
@@ -21,54 +20,48 @@ func CreateSessionManager(lifetime time.Duration) *scs.SessionManager {
return Session return Session
} }
func showLoginPage(w http.ResponseWriter, r *http.Request, failed bool) { func showLoginPage(w http.ResponseWriter, r *http.Request, success bool, errorMsg string) {
r = r.WithContext(context.WithValue(r.Context(), "session", Session)) r = r.WithContext(context.WithValue(r.Context(), "session", Session))
if helper.GetEnv("GO_ENV", "production") == "debug" {
// http.Redirect(w, r, "/time", http.StatusSeeOther)
templates.LoginPage(failed).Render(r.Context(), w)
}
if Session.Exists(r.Context(), "user") { if Session.Exists(r.Context(), "user") {
http.Redirect(w, r, "/time", http.StatusSeeOther) http.Redirect(w, r, "/time", http.StatusSeeOther)
} }
templates.LoginPage(failed).Render(r.Context(), w) templates.LoginPage(success, errorMsg).Render(r.Context(), w)
} }
func loginUser(w http.ResponseWriter, r *http.Request) { func loginUser(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
log.Println("Error parsing form!", err) log.Println("Error parsing form!", err)
http.Error(w, "Internal error", http.StatusBadRequest) showLoginPage(w, r, false, "Internal error!")
return return
} }
_personal_nummer := r.FormValue("personal_nummer") _personal_nummer := r.FormValue("personal_nummer")
if _personal_nummer == "" { if _personal_nummer == "" {
log.Println("No personal_nummer provided!") log.Println("No personal_nummer provided!")
http.Error(w, "No personal_nummer provided", http.StatusBadRequest) showLoginPage(w, r, false, "Keine Personalnummer gesetzt.")
return return
} }
personal_nummer, err := strconv.Atoi(_personal_nummer) personal_nummer, err := strconv.Atoi(_personal_nummer)
if err != nil { if err != nil {
log.Println("Cannot parse personal nubmer!") log.Println("Cannot parse personal nubmer!")
http.Error(w, "Cannot parse number", http.StatusBadRequest) showLoginPage(w, r, false, "Personalnummer ist nicht valide gesetzt.")
return return
} }
user, err := models.GetUserByPersonalNr(personal_nummer) user, err := models.GetUserByPersonalNr(personal_nummer)
if err != nil { if err != nil {
log.Println("No user found under this personal number!") log.Println("No user found under this personal number!", err)
http.Error(w, "No user found!", http.StatusNotFound) showLoginPage(w, r, false, "Nutzer unter dieser Personalnummer nicht gefunden.")
return
} }
password := r.FormValue("password") password := r.FormValue("password")
if user.Login(password) { if user.Login(password) {
log.Printf("New succesfull user login from %s %s!\n", user.Vorname, user.Name) log.Printf("New succesfull user login from %s %s (%d)!\n", user.Vorname, user.Name, user.PersonalNummer)
Session.Put(r.Context(), "user", user.PersonalNummer) Session.Put(r.Context(), "user", user.PersonalNummer)
Session.Commit(r.Context())
http.Redirect(w, r, "/time", http.StatusSeeOther) //with this browser always uses GET http.Redirect(w, r, "/time", http.StatusSeeOther) //with this browser always uses GET
} else {
showLoginPage(w, r, true)
return
} }
showLoginPage(w, r, false) showLoginPage(w, r, false, "")
} }
func logoutUser(w http.ResponseWriter, r *http.Request) { func logoutUser(w http.ResponseWriter, r *http.Request) {

View File

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

View File

@@ -19,7 +19,7 @@ func UserHandler(w http.ResponseWriter, r *http.Request) {
func LoginHandler(w http.ResponseWriter, r *http.Request) { func LoginHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
showLoginPage(w, r, false) showLoginPage(w, r, true, "")
case http.MethodPost: case http.MethodPost:
loginUser(w, r) loginUser(w, r)
default: default:
@@ -29,6 +29,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
func UserSettingsHandler(w http.ResponseWriter, r *http.Request) { func UserSettingsHandler(w http.ResponseWriter, r *http.Request) {
helper.RequiresLogin(Session, w, r) helper.RequiresLogin(Session, w, r)
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
showUserPage(w, r, 0) showUserPage(w, r, 0)

View File

@@ -1,6 +1,6 @@
module arbeitszeitmessung module arbeitszeitmessung
go 1.24.5 go 1.24.7
require github.com/lib/pq v1.10.9 require github.com/lib/pq v1.10.9
@@ -17,4 +17,5 @@ require (
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect
golang.org/x/sys v0.36.0 // indirect
) )

View File

@@ -68,7 +68,7 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,24 @@
package logs
import (
"log"
"os"
"time"
)
type FileLog struct {
Logger *log.Logger
Close func() error
}
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"
logFile, err := os.OpenFile(LOG_FILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Panic(err)
}
return log.New(logFile, "", log.LstdFlags), logFile.Close
}

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

@@ -16,8 +16,21 @@ func GetMonday(ts time.Time) time.Time {
return ts return ts
} }
// Converts duration to string func IsWeekend(ts time.Time) bool {
return ts.Weekday() == time.Saturday || ts.Weekday() == time.Sunday
}
func GetKW(t time.Time) int {
_, kw := t.ISOWeek()
return kw
}
func FormatDuration(d time.Duration) string { 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()) hours := int(d.Abs().Hours())
minutes := int(d.Abs().Minutes()) % 60 minutes := int(d.Abs().Minutes()) % 60
sign := "" sign := ""
@@ -32,6 +45,13 @@ func FormatDuration(d time.Duration) string {
case minutes > 0: case minutes > 0:
return fmt.Sprintf("%s%dmin", sign, minutes) return fmt.Sprintf("%s%dmin", sign, minutes)
default: default:
if fill {
return "0min"
}
return "" return ""
} }
} }
func IsSameDate(a, b time.Time) bool {
return a.Truncate(24 * time.Hour).Equal(b.Truncate(24 * time.Hour))
}

View File

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

View File

@@ -43,6 +43,7 @@ func main() {
// handles the different http routes // handles the different http routes
server.HandleFunc("/time/new", endpoints.TimeCreateHandler) server.HandleFunc("/time/new", endpoints.TimeCreateHandler)
server.Handle("/absence", ParamsMiddleware(endpoints.AbsencHandler))
server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler)) server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler))
server.HandleFunc("/logout", endpoints.LogoutHandler) server.HandleFunc("/logout", endpoints.LogoutHandler)
server.HandleFunc("/user/{action}", endpoints.UserHandler) server.HandleFunc("/user/{action}", endpoints.UserHandler)
@@ -50,6 +51,7 @@ func main() {
// server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) // server.HandleFunc("/user/settings", endpoints.UserSettingsHandler)
server.HandleFunc("/team", endpoints.TeamHandler) server.HandleFunc("/team", endpoints.TeamHandler)
server.HandleFunc("/team/presence", endpoints.TeamPresenceHandler) server.HandleFunc("/team/presence", endpoints.TeamPresenceHandler)
server.HandleFunc("/pdf", endpoints.PDFHandler)
server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect)) server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect))
server.Handle("/static/", http.StripPrefix("/static/", fs)) server.Handle("/static/", http.StripPrefix("/static/", fs))

View File

@@ -1,23 +1,25 @@
package models package models
import ( import (
"encoding/json"
"errors" "errors"
"log" "log"
"time" "time"
) )
type AbsenceType struct { type AbsenceType struct {
Id int8 Id int8 `json:"abwesenheit_id"`
Name string Name string `json:"abwesenheit_name"`
WorkTime float32 WorkTime int8 `json:"arbeitszeit_equivalent"`
} }
type Absence struct { type Absence struct {
Day time.Time
CounterId int CounterId int
CardUID string CardUID string
AbwesenheitTyp AbsenceType AbwesenheitTyp AbsenceType
Datum time.Time DateFrom time.Time
// Comment string DateTo time.Time
} }
func NewAbsence(card_uid string, abwesenheit_typ int, datum time.Time) (Absence, error) { 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{ return Absence{
CardUID: card_uid, CardUID: card_uid,
AbwesenheitTyp: AbsenceType{0, "Custom absence", 100}, AbwesenheitTyp: AbsenceType{0, "Custom absence", 100},
Datum: datum, DateFrom: datum,
}, nil }, nil
} }
_absenceType, ok := GetAbsenceTypesCached()[int8(abwesenheit_typ)] _absenceType, ok := GetAbsenceTypesCached()[int8(abwesenheit_typ)]
@@ -35,18 +37,75 @@ func NewAbsence(card_uid string, abwesenheit_typ int, datum time.Time) (Absence,
return Absence{ return Absence{
CardUID: card_uid, CardUID: card_uid,
AbwesenheitTyp: _absenceType, AbwesenheitTyp: _absenceType,
Datum: datum, DateFrom: datum,
}, nil }, 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) 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 { 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 { if err != nil {
log.Println("Error preparing sql Statement", err) log.Println("Error preparing sql Statement", err)
return err return err
} }
defer qStr.Close() 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 { if err != nil {
log.Println("Error executing insert statement", err) log.Println("Error executing insert statement", err)
return err return err
@@ -54,14 +113,31 @@ func (a *Absence) Insert() error {
return nil 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) { func GetAbsenceById(counterId int) (Absence, error) {
var absence Absence = Absence{CounterId: counterId} 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 { if err != nil {
return absence, err return absence, err
} }
defer qStr.Close() 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 { if err != nil {
return absence, err return absence, err
} }
@@ -70,7 +146,32 @@ func GetAbsenceById(counterId int) (Absence, error) {
func GetAbsencesByCardUID(card_uid string, tsFrom time.Time, tsTo time.Time) ([]Absence, error) { func GetAbsencesByCardUID(card_uid string, tsFrom time.Time, tsTo time.Time) ([]Absence, error) {
var absences []Absence 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 { if err != nil {
return absences, err return absences, err
} }
@@ -82,15 +183,16 @@ func GetAbsencesByCardUID(card_uid string, tsFrom time.Time, tsTo time.Time) ([]
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var absence Absence 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 return absences, err
} }
absence.AbwesenheitTyp, err = GetAbsenceTypeById(absence.AbwesenheitTyp.Id) err = json.Unmarshal(abwesenheitsTyp, &absence.AbwesenheitTyp)
if err == nil { if err != nil {
absences = append(absences, absence) log.Println("Error parsing abwesenheitsTyp to JSON!", err)
} else { return absences, nil
log.Println("Cannot populate absence type!", err)
} }
absences = append(absences, absence)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return absences, err return absences, err
@@ -98,6 +200,27 @@ func GetAbsencesByCardUID(card_uid string, tsFrom time.Time, tsTo time.Time) ([]
return absences, nil 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) { func GetAbsenceTypes() (map[int8]AbsenceType, error) {
var types = make(map[int8]AbsenceType) var types = make(map[int8]AbsenceType)
qStr, err := DB.Prepare("SELECT abwesenheit_id, abwesenheit_name, arbeitszeit_equivalent FROM s_abwesenheit_typen;") qStr, err := DB.Prepare("SELECT abwesenheit_id, abwesenheit_name, arbeitszeit_equivalent FROM s_abwesenheit_typen;")

View File

@@ -2,11 +2,11 @@ package models
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/helper/logs"
"database/sql" "database/sql"
"fmt" "fmt"
"log" "log"
"net/url" "net/url"
"sort"
"strconv" "strconv"
"time" "time"
) )
@@ -14,8 +14,8 @@ import (
type SameBookingError struct{} type SameBookingError struct{}
type BookingType struct { type BookingType struct {
Id int8 Id int8 `json:"anwesenheit_id"`
Name string Name string `json:"anwesenheit_name"`
} }
func (e SameBookingError) Error() string { func (e SameBookingError) Error() string {
@@ -28,7 +28,7 @@ type Booking struct {
CheckInOut int16 `json:"check_in_out"` CheckInOut int16 `json:"check_in_out"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
CounterId int `json:"counter_id"` CounterId int `json:"counter_id"`
BookingType BookingType `json:"booking_type"` BookingType BookingType `json:"anwesenheit_typ"`
} }
type IDatabase interface { type IDatabase interface {
@@ -162,43 +162,13 @@ func (b *Booking) GetBookingsByCardID(card_uid string, tsFrom time.Time, tsTo ti
return bookings, nil 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() { 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;`)) 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 { if err != nil {
log.Fatalf("Error preparing query: %v", err) log.Fatalf("Error preparing query: %v", err)
return return
} }
_, err = qStr.Query(b.CounterId, b.CardUID, b.GeraetID, b.CheckInOut, b.Timestamp) _, err = qStr.Query(b.CounterId, b.CardUID, b.GeraetID, b.CheckInOut, b.Timestamp)
if err != nil { if err != nil {
log.Fatalf("Error executing query: %v", err) log.Fatalf("Error executing query: %v", err)
@@ -231,6 +201,8 @@ func (b *Booking) GetBookingType() string {
} }
func (b *Booking) Update(nb Booking) { func (b *Booking) Update(nb Booking) {
auditLog, closeLog := logs.NewAudit()
defer closeLog()
if b.CheckInOut != nb.CheckInOut && nb.CheckInOut != 0 { if b.CheckInOut != nb.CheckInOut && nb.CheckInOut != 0 {
b.CheckInOut = nb.CheckInOut b.CheckInOut = nb.CheckInOut
} }
@@ -241,6 +213,7 @@ func (b *Booking) Update(nb Booking) {
b.GeraetID = nb.GeraetID b.GeraetID = nb.GeraetID
} }
if b.Timestamp != nb.Timestamp { if b.Timestamp != nb.Timestamp {
auditLog.Printf("Änderung in Buchung %d von '%s': Buchungszeit (%s -> %s).", b.CounterId, b.CardUID, b.Timestamp.Format("15:04"), nb.Timestamp.Format("15:04)"))
b.Timestamp = nb.Timestamp b.Timestamp = nb.Timestamp
} }
} }
@@ -281,15 +254,19 @@ func (b *Booking) UpdateTime(newTime time.Time) {
if b.CheckInOut == 254 { if b.CheckInOut == 254 {
newBooking.CheckInOut = 4 newBooking.CheckInOut = 4
} }
log.Println("Updating")
b.Update(newBooking) b.Update(newBooking)
// TODO Check verify // TODO Check verify
b.Verify() if b.Verify() {
b.Save() b.Save()
} else {
log.Println("Cannot save updated booking!", b.ToString())
}
// b.Verify()
// b.Save()
} }
func (b *Booking) ToString() string { 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) { 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 { func SetupDBFixture(t *testing.T) *DBFixture {
t.Helper() 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") dbHost := helper.GetEnv("POSTGRES_HOST", "localhost")
dbPort := helper.GetEnv("POSTGRES_PORT", "5433") dbPort := helper.GetEnv("POSTGRES_PORT", "5433")
@@ -36,10 +39,12 @@ func SetupDBFixture(t *testing.T) *DBFixture {
t.Fatalf("failed to connect to database: %v", err) t.Fatalf("failed to connect to database: %v", err)
} }
// err = MigrateDB(db, "file://../../migrations") defer db.Close()
// if err != nil && err != migrate.ErrNoChange {
// t.Fatalf("Failed to migrate database: %v", err) err = MigrateDB(db, "file://../../migrations")
// } if err != nil && err != migrate.ErrNoChange {
t.Fatalf("Failed to migrate database: %v", err)
}
tx, err := db.Begin() tx, err := db.Begin()
if err != nil { if err != nil {

View File

@@ -13,16 +13,16 @@ import (
) )
type User struct { type User struct {
CardUID string `json:"card_uid"` CardUID string //`json:"card_uid"`
Name string `json:"name"` Name string `json:"name"`
Vorname string `json:"vorname"` Vorname string `json:"vorname"`
PersonalNummer int `json:"personal_nummer"` PersonalNummer int //`json:"personal_nummer"`
ArbeitszeitPerTag float32 `json:"arbeitszeit_per_tag"` ArbeitszeitPerTag float32 //`json:"arbeitszeit_per_tag"`
ArbeitszeitPerWoche float32 `json:"arbeitszeit_per_woche"` ArbeitszeitPerWoche float32 //`json:"arbeitszeit_per_woche"`
Overtime time.Duration 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 user User
var err error var err error
if helper.GetEnv("GO_ENV", "production") == "debug" { if helper.GetEnv("GO_ENV", "production") == "debug" {
@@ -44,19 +44,16 @@ func (u *User) GetUserFromSession(Session *scs.SessionManager, ctx context.Conte
// Returns the actual overtime for this moment // Returns the actual overtime for this moment
func (u *User) GetReportedOvertime() (time.Duration, error) { func (u *User) GetReportedOvertime() (time.Duration, error) {
var overtime time.Duration var overtime time.Duration
var overtimeReport float64
qStr, err := DB.Prepare("SELECT COALESCE(SUM(ueberstunden), 0) AS total_ueberstunden FROM wochen_report WHERE personal_nummer = $1;") qStr, err := DB.Prepare("SELECT COALESCE(SUM(EXTRACT(EPOCH FROM ueberstunden) * 1000000000)::BIGINT, 0) AS total_ueberstunden_ns FROM wochen_report WHERE personal_nummer = $1;")
if err != nil { if err != nil {
return 0, err return 0, err
} }
defer qStr.Close() defer qStr.Close()
err = qStr.QueryRow(u.PersonalNummer).Scan(&overtimeReport) err = qStr.QueryRow(u.PersonalNummer).Scan(&overtime)
if err != nil { if err != nil {
return 0, err return 0, err
} }
overtime = time.Duration(overtimeReport * float64(time.Hour)).Round(time.Minute)
log.Println("Overtime from wochen_report: ", overtime)
return overtime, nil return overtime, nil
} }
@@ -88,6 +85,15 @@ func (u *User) GetAll() ([]User, error) {
return users, nil return users, nil
} }
// Returns the worktime per day rounded to minutes
func (u *User) ArbeitszeitProTag() time.Duration {
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 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 // Returns false if there is no booking today or the user is already booked out of the system
func (u *User) CheckAnwesenheit() bool { func (u *User) CheckAnwesenheit() bool {
@@ -167,7 +173,7 @@ func (u *User) ChangePass(password, newPassword string) (bool, error) {
func (u *User) GetTeamMembers() ([]User, error) { func (u *User) GetTeamMembers() ([]User, error) {
var teamMembers []User var teamMembers []User
qStr, err := DB.Prepare(`SELECT personal_nummer FROM s_personal_daten WHERE vorgesetzter_pers_nr = $1`) qStr, err := DB.Prepare(`SELECT personal_nummer FROM s_personal_daten WHERE vorgesetzter_pers_nr = $1 ORDER BY "nachname";`)
if err != nil { if err != nil {
return teamMembers, err return teamMembers, err
} }
@@ -201,17 +207,6 @@ func (u *User) IsTeamLeader() bool {
return len(team) > 0 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 // gets the first week, that needs to be submitted
func (u *User) GetNextWeek() WorkWeek { func (u *User) GetNextWeek() WorkWeek {
var week WorkWeek var week WorkWeek

View File

@@ -6,16 +6,20 @@ import (
"testing" "testing"
) )
var testUser models.User = models.User{Vorname: "Kim", Name: "Mustermensch", PersonalNummer: 456, CardUID: "aaaa-aaaa", ArbeitszeitPerTag: 8} var testUser models.User = models.User{Vorname: "Kim", Name: "Mustermensch", PersonalNummer: 456, CardUID: "aaaa-aaaa", ArbeitszeitPerTag: 8, ArbeitszeitPerWoche: 40}
func SetupUserFixture(t *testing.T, db models.IDatabase) { func SetupUserFixture(t *testing.T, db models.IDatabase) {
t.Helper() t.Helper()
db.Exec(`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 _, err := db.Exec(`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
(456, 't', 'Kim', 'Mustermensch', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'aaaa-aaaa', 1, 8, '07:00:00', '20:00:00', 0);`) (456, 't', 'Kim', 'Mustermensch', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'aaaa-aaaa', 1, 8, 40, '07:00:00', '20:00:00', 0);`)
if err != nil {
t.Fatal("SetupUserFixture:", err)
}
} }
func TestGetUserByPersonalNr(t *testing.T) { func TestGetUserByPersonalNr(t *testing.T) {
tc := SetupDBFixture(t) tc := SetupDBFixture(t)
SetupUserFixture(t, tc.Database) SetupUserFixture(t, tc.Database)
models.DB = tc.Database models.DB = tc.Database
@@ -44,12 +48,12 @@ func TestCheckAnwesenheit(t *testing.T) {
if actual = testUser.CheckAnwesenheit(); actual != false { if actual = testUser.CheckAnwesenheit(); actual != false {
t.Errorf("Checkabwesenheit with no booking should be false but is %t", actual) t.Errorf("Checkabwesenheit with no booking should be false but is %t", actual)
} }
tc.Database.Exec("INSERT INTO anwesenheit (timestamp, card_uid, check_in_out, geraet_id) VALUES (NOW() - INTERVAL '2 minute', 'aaaa-aaaa', 1, 1);") tc.Database.Exec("INSERT INTO anwesenheit (timestamp, card_uid, check_in_out, geraet_id, anwesenheit_typ) VALUES (NOW() - INTERVAL '2 minute', 'aaaa-aaaa', 1, 1, 1);")
if actual = testUser.CheckAnwesenheit(); actual != true { if actual = testUser.CheckAnwesenheit(); actual != true {
t.Errorf("Checkabwesenheit with 'kommen' booking should be true but is %t", actual) t.Errorf("Checkabwesenheit with 'kommen' booking should be true but is %t", actual)
} }
tc.Database.Exec("INSERT INTO anwesenheit (timestamp, card_uid, check_in_out, geraet_id) VALUES (NOW() - INTERVAL '1 minute', 'aaaa-aaaa', 2, 1);") tc.Database.Exec("INSERT INTO anwesenheit (timestamp, card_uid, check_in_out, geraet_id, anwesenheit_typ) VALUES (NOW() - INTERVAL '1 minute', 'aaaa-aaaa', 2, 1, 1);")
if actual = testUser.CheckAnwesenheit(); actual != false { if actual = testUser.CheckAnwesenheit(); actual != false {
t.Errorf("Checkabwesenheit with 'gehen' booking should be false but is %t", actual) t.Errorf("Checkabwesenheit with 'gehen' booking should be false but is %t", actual)
} }

View File

@@ -2,52 +2,186 @@ package models
import ( import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"database/sql"
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"strconv" "sort"
"time" "time"
) )
type IWorkDay interface {
Date() time.Time
TimeWorkVirtual(User) time.Duration
TimeWorkReal(User) time.Duration
TimePauseReal(User) (work, pause time.Duration)
TimeOvertimeReal(User) time.Duration
GetAllWorkTimesVirtual(User) (work, pause, overtime time.Duration)
ToString() string
IsWorkDay() bool
IsKurzArbeit() bool
GetDayProgress(User) int8
RequiresAction() bool
}
type WorkDay struct { type WorkDay struct {
Day time.Time `json:"day"` Day time.Time `json:"day"`
Bookings []Booking `json:"bookings"` Bookings []Booking `json:"bookings"`
workTime time.Duration workTime time.Duration
pauseTime time.Duration pauseTime time.Duration
realWorkTime time.Duration
realPauseTime time.Duration
TimeFrom time.Time TimeFrom time.Time
TimeTo time.Time TimeTo time.Time
Absence Absence kurzArbeit bool
kurzArbeitAbsence Absence
} }
func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay { func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay {
var allDays map[string]IWorkDay = make(map[string]IWorkDay)
var sortedDays []IWorkDay
for _, day := range GetWorkDays(user, tsFrom, tsTo) {
allDays[day.Date().Format("2006-01-02")] = &day
}
absences, err := GetAbsencesByCardUID(user.CardUID, tsFrom, tsTo)
if err != nil {
log.Println("Error gettings absences for all Days!", err)
return sortedDays
}
for _, day := range absences {
if helper.IsWeekend(day.Date()) {
continue
}
if day.AbwesenheitTyp.WorkTime == 1 {
if workDay, ok := allDays[day.Date().Format("2006-01-02")].(*WorkDay); ok {
workDay.kurzArbeit = true
workDay.kurzArbeitAbsence = day
}
} else {
allDays[day.Date().Format("2006-01-02")] = &day
}
}
for _, day := range allDays {
sortedDays = append(sortedDays, day)
}
if orderedForward {
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) 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())
}
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("2006-01-02"), 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 workDays []WorkDay
var workSec, pauseSec float64 var workSec, pauseSec float64
qStr, err := DB.Prepare(` qStr, err := DB.Prepare(`
WITH all_days AS ( WITH all_days AS (
SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date),
),
ordered_bookings AS ( ordered_bookings AS (
SELECT SELECT
timestamp::DATE AS work_date, a.timestamp::DATE AS work_date,
timestamp, a.timestamp,
check_in_out, a.check_in_out,
counter_id, a.counter_id,
LAG(timestamp) OVER (PARTITION BY card_uid, timestamp::DATE ORDER BY timestamp) AS prev_timestamp, a.anwesenheit_typ,
LAG(check_in_out) OVER (PARTITION BY card_uid, timestamp::DATE ORDER BY timestamp) AS prev_check sat.anwesenheit_name AS anwesenheit_typ_name,
FROM anwesenheit LAG(a.timestamp) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_timestamp,
WHERE card_uid = $1 LAG(a.check_in_out) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_check
AND timestamp::DATE >= $2 FROM anwesenheit a
AND timestamp::DATE <= $3 LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id
), WHERE a.card_uid = $1
abwesenheiten AS ( AND a.timestamp::DATE >= $2
SELECT AND a.timestamp::DATE <= $3
datum::DATE AS work_date,
abwesenheit_typ
FROM abwesenheit
WHERE card_uid = $1
AND datum::DATE >= $2
AND datum::DATE <= $3
) )
SELECT SELECT
d.work_date, d.work_date,
@@ -56,7 +190,7 @@ func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay
COALESCE( COALESCE(
EXTRACT(EPOCH FROM SUM( EXTRACT(EPOCH FROM SUM(
CASE CASE
WHEN b.prev_check IN (1, 3) AND b.check_in_out IN (2, 4, 255) WHEN b.prev_check IN (1, 3) AND b.check_in_out IN (2, 4, 254)
THEN b.timestamp - b.prev_timestamp THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0' ELSE INTERVAL '0'
END END
@@ -65,7 +199,7 @@ func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay
COALESCE( COALESCE(
EXTRACT(EPOCH FROM SUM( EXTRACT(EPOCH FROM SUM(
CASE CASE
WHEN b.prev_check IN (2, 4, 255) AND b.check_in_out IN (1, 3) WHEN b.prev_check IN (2, 4, 254) AND b.check_in_out IN (1, 3)
THEN b.timestamp - b.prev_timestamp THEN b.timestamp - b.prev_timestamp
ELSE INTERVAL '0' ELSE INTERVAL '0'
END END
@@ -74,13 +208,16 @@ func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay
COALESCE(jsonb_agg(jsonb_build_object( COALESCE(jsonb_agg(jsonb_build_object(
'check_in_out', b.check_in_out, 'check_in_out', b.check_in_out,
'timestamp', b.timestamp, 'timestamp', b.timestamp,
'counter_id', b.counter_id 'counter_id', b.counter_id,
) ORDER BY b.timestamp), '[]'::jsonb) AS bookings, 'anwesenheit_typ', b.anwesenheit_typ,
a.abwesenheit_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 FROM all_days d
LEFT JOIN ordered_bookings b ON d.work_date = b.work_date 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
GROUP BY d.work_date, a.abwesenheit_typ
ORDER BY d.work_date ASC;`) ORDER BY d.work_date ASC;`)
if err != nil { if err != nil {
@@ -89,18 +226,17 @@ func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay
} }
defer qStr.Close() defer qStr.Close()
rows, err := qStr.Query(card_uid, tsFrom, tsTo) rows, err := qStr.Query(user.CardUID, tsFrom, tsTo)
if err != nil { if err != nil {
log.Println("Error getting rows!") log.Println("Error getting rows!")
return workDays return workDays
} }
defer rows.Close() defer rows.Close()
emptyDays, _ := strconv.ParseBool(helper.GetEnv("EMPTY_DAYS", "false")) // emptyDays, _ := strconv.ParseBool(helper.GetEnv("EMPTY_DAYS", "false"))
for rows.Next() { for rows.Next() {
var workDay WorkDay var workDay WorkDay
var bookings []byte var bookings []byte
var absenceType sql.NullInt16 if err := rows.Scan(&workDay.Day, &workDay.TimeFrom, &workDay.TimeTo, &workSec, &pauseSec, &bookings); err != nil {
if err := rows.Scan(&workDay.Day, &workDay.TimeFrom, &workDay.TimeTo, &workSec, &pauseSec, &bookings, &absenceType); err != nil {
log.Println("Error scanning row!", err) log.Println("Error scanning row!", err)
return workDays return workDays
} }
@@ -115,99 +251,55 @@ func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay
if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 { if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 {
workDay.Bookings = []Booking{} workDay.Bookings = []Booking{}
} }
workDay.TimePauseReal(user)
if absenceType.Valid { if len(workDay.Bookings) > 1 || !helper.IsWeekend(workDay.Date()) {
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 {
workDays = append(workDays, workDay) 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 { if err = rows.Err(); err != nil {
log.Println("Error in workday rows!", err)
return workDays return workDays
} }
return workDays return workDays
} }
func (d *WorkDay) calcPauseTime() { func (d *WorkDay) GetAllWorkTimesReal(user User) (work, pause, overtime time.Duration) {
if d.workTime > 6*time.Hour && d.pauseTime < 45*time.Minute { if d.pauseTime == 0 || d.workTime == 0 {
if d.workTime <= (9*time.Hour) && d.pauseTime < 30*time.Minute { d.TimePauseReal(user)
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
}
} }
return d.workTime.Round(time.Minute), d.pauseTime.Round(time.Minute), d.TimeOvertimeReal(user)
} }
// Gets the duration someone worked that day func (d *WorkDay) GetAllWorkTimesVirtual(user User) (work, pause, overtime time.Duration) {
func (d *WorkDay) getWorkTime() { _, pause, overtime = d.GetAllWorkTimesReal(user)
if len(d.Bookings) < 1 { return d.TimeWorkVirtual(user), pause, overtime
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
} }
// returns bool wheter the workday was ended with an automatic logout // returns bool wheter the workday was ended with an automatic logout
func (d *WorkDay) RequiresAction() bool { func (d *WorkDay) RequiresAction() bool {
if len(d.Bookings) > 0 { if len(d.Bookings) == 0 {
return d.Bookings[len(d.Bookings)-1].CheckInOut == 254
}
return false return false
}
// 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) CalcOvertime(user User) time.Duration {
var overtime time.Duration
overtime = d.workTime - time.Duration(user.ArbeitszeitPerTag*float32(time.Hour)).Round(time.Minute)
// weekday is WE
if (d.Day.Weekday() == 6 || d.Day.Weekday() == 0) && len(d.Bookings) == 0 {
overtime = 0
} }
return overtime return d.Bookings[len(d.Bookings)-1].CheckInOut == 254
} }
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 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("2006-01-02", "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,7 +1,6 @@
package models package models
import ( import (
"arbeitszeitmessung/helper"
"database/sql" "database/sql"
"errors" "errors"
"log" "log"
@@ -14,6 +13,7 @@ type WorkWeek struct {
Id int Id int
WorkDays []WorkDay WorkDays []WorkDay
Absences []Absence Absences []Absence
Days []IWorkDay
User User User User
WeekStart time.Time WeekStart time.Time
Worktime time.Duration Worktime time.Duration
@@ -39,20 +39,25 @@ func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek {
Status: WeekStatusNone, Status: WeekStatusNone,
} }
if populate { if populate {
week.PopulateWithBookings(0, 0) week.PopulateWithDays(0, 0)
} }
return week return week
} }
func (w *WorkWeek) PopulateWithBookings(overtime time.Duration, worktime time.Duration) { func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Duration) {
w.WorkDays = (*WorkDay).GetWorkDays(nil, w.User.CardUID, w.WeekStart, w.WeekStart.Add(7*24*time.Hour)) log.Println("Got Days with overtime and worktime", worktime, overtime)
if absences, err := GetAbsencesByCardUID(w.User.CardUID, w.WeekStart, w.WeekStart.Add(7*24*time.Hour)); err == nil { w.Days = GetDays(w.User, w.WeekStart, w.WeekStart.Add(6*24*time.Hour), false)
w.Absences = absences log.Println(w.Worktime)
} else {
log.Printf("Error populating absences in workWeek (%s): %v", w.WeekStart, err) for _, day := range w.Days {
log.Println(day.TimeWorkVirtual(w.User))
w.Worktime += day.TimeWorkVirtual(w.User)
} }
w.Worktime = w.aggregateWorkTime() log.Println("Calculated new worktime", w.Worktime)
w.Overtime = w.Worktime - time.Duration(w.User.ArbeitszeitPerWoche*float32(time.Hour)).Round(time.Minute) w.Overtime = w.Worktime - w.User.ArbeitszeitProWoche()
w.Worktime = w.Worktime.Round(time.Minute)
w.Overtime = w.Overtime.Round(time.Minute)
if overtime == 0 && worktime == 0 { if overtime == 0 && worktime == 0 {
return return
@@ -96,25 +101,22 @@ func (w *WorkWeek) CheckStatus() WeekStatus {
return w.Status return w.Status
} }
func (w *WorkWeek) GetWorkHourString() string {
return helper.FormatDuration(w.Worktime)
}
func (w *WorkWeek) aggregateWorkTime() time.Duration { func (w *WorkWeek) aggregateWorkTime() time.Duration {
var workTime time.Duration var workTime time.Duration
for _, day := range w.WorkDays { for _, day := range w.WorkDays {
workTime += day.workTime workTime += day.workTime
} }
for _, absences := range w.Absences { // for _, absence := range w.Absences {
absenceWorkTime := absences.AbwesenheitTyp.WorkTime - (absences.AbwesenheitTyp.WorkTime - w.User.ArbeitszeitPerTag) // workTime Equivalent of Absence is capped at user Worktime per Day // log.Println(absence)
workTime += time.Duration(absenceWorkTime * float32(time.Hour)).Round(time.Minute) // 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 return workTime
} }
func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek { func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek {
var weeks []WorkWeek var weeks []WorkWeek
qStr, err := DB.Prepare(`SELECT id, woche_start::DATE, arbeitszeit, ueberstunden FROM wochen_report WHERE bestaetigt = FALSE AND personal_nummer = $1;`) qStr, err := DB.Prepare(`SELECT id, woche_start::DATE, (EXTRACT(epoch FROM arbeitszeit)*1000000000)::BIGINT, (EXTRACT(epoch FROM ueberstunden)*1000000000)::BIGINT FROM wochen_report WHERE bestaetigt = FALSE AND personal_nummer = $1;`)
if err != nil { if err != nil {
log.Println("Error preparing SQL statement", err) log.Println("Error preparing SQL statement", err)
return weeks return weeks
@@ -129,15 +131,13 @@ func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var week WorkWeek = WorkWeek{User: user} var week WorkWeek = WorkWeek{User: user}
var workHours, overtime sql.NullFloat64 var workTime, overTime time.Duration
if err := rows.Scan(&week.Id, &week.WeekStart, &workHours, &overtime); err != nil { if err := rows.Scan(&week.Id, &week.WeekStart, &workTime, &overTime); err != nil {
log.Println("Error scanning row!", err) log.Println("Error scanning row!", err)
return weeks return weeks
} }
if workHours.Valid && overtime.Valid { week.PopulateWithDays(workTime, overTime)
week.PopulateWithBookings(time.Duration(workHours.Float64*float64(time.Hour)).Round(time.Minute), time.Duration(overtime.Float64*float64(time.Hour)).Round(time.Minute))
}
weeks = append(weeks, week) weeks = append(weeks, week)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
@@ -160,19 +160,19 @@ func (w *WorkWeek) SendWeek() error {
} }
if w.CheckStatus() != WeekStatusNone { if w.CheckStatus() != WeekStatusNone {
qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE, arbeitszeit = $3, ueberstunden = $4 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) WHERE personal_nummer = $1 AND woche_start = $2;`)
if err != nil { if err != nil {
log.Println("Error preparing SQL statement", err) log.Println("Error preparing SQL statement", err)
return err return err
} }
} else { } else {
qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden) VALUES ($1, $2, $3, $4);`) 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));`)
if err != nil { if err != nil {
log.Println("Error preparing SQL statement", err) log.Println("Error preparing SQL statement", err)
return err return err
} }
} }
_, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, w.Worktime.Hours(), w.Overtime.Hours()) _, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, int64(w.Worktime), int64(w.Overtime))
if err != nil { if err != nil {
log.Println("Error executing query!", err) log.Println("Error executing query!", err)
return err return err
@@ -193,3 +193,11 @@ func (w *WorkWeek) Accept() error {
} }
return nil return nil
} }
func (w *WorkWeek) RequiresAction() bool {
var requiresAction bool = false
for _, day := range w.Days {
requiresAction = requiresAction || day.RequiresAction()
}
return requiresAction
}

View File

@@ -28,6 +28,7 @@ func TestNewWorkWeekNoPopulate(t *testing.T) {
} }
func TestCheckStatus(t *testing.T) { func TestCheckStatus(t *testing.T) {
SetupDBFixture(t)
testWeek := SetupWorkWeekFixture(t) testWeek := SetupWorkWeekFixture(t)
testCases := []struct { testCases := []struct {
name string name string

View File

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

View File

@@ -1,7 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
@source "../templates/*.templ"; @source "../templates/*.templ";
@plugin "@iconify/tailwind4" { @plugin "@iconify/tailwind4" {
scale: 1.5; scale: 1.25;
} }
@theme { @theme {
@@ -30,10 +30,18 @@
--color-text-950: #000000; --color-text-950: #000000;
} }
@layer base {
body {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
background-color: white;
}
}
@layer components { @layer components {
.grid-main { .grid-main {
display: grid; display: grid;
grid-template-columns: 2fr auto 1fr; grid-template-columns: 4fr 3fr 3fr 1fr;
align-items: stretch; align-items: stretch;
} }
@@ -70,11 +78,22 @@
border-style: var(--tw-border-style); border-style: var(--tw-border-style);
border-width: 1px; border-width: 1px;
border-color: var(--color-neutral-800); border-color: var(--color-neutral-800);
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; transition-property:
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); color, background-color, border-color, outline-color,
text-decoration-color, fill, stroke, --tw-gradient-from,
--tw-gradient-via, --tw-gradient-to;
transition-timing-function: var(
--tw-ease,
var(--default-transition-timing-function)
);
transition-duration: var(--tw-duration, var(--default-transition-duration)); transition-duration: var(--tw-duration, var(--default-transition-duration));
} }
input.btn,
select.btn {
transition-duration: 300ms;
}
.btn:hover { .btn:hover {
color: var(--color-white); color: var(--color-white);
background-color: var(--color-neutral-700); background-color: var(--color-neutral-700);
@@ -85,9 +104,46 @@
pointer-events: none; pointer-events: none;
} }
.btn:active, input.btn,
.btn:focus { select.btn {
background-color: var(--color-neutral-700); 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) { @media (width >=48rem) {

View File

@@ -9,16 +9,18 @@
"Courier New", monospace; "Courier New", monospace;
--color-red-500: oklch(63.7% 0.237 25.331); --color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325); --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-orange-500: oklch(70.5% 0.213 47.604);
--color-purple-600: oklch(55.8% 0.288 302.321); --color-purple-600: oklch(55.8% 0.288 302.321);
--color-slate-700: oklch(37.2% 0.044 257.287);
--color-neutral-100: oklch(97% 0 0); --color-neutral-100: oklch(97% 0 0);
--color-neutral-200: oklch(92.2% 0 0); --color-neutral-200: oklch(92.2% 0 0);
--color-neutral-300: oklch(87% 0 0); --color-neutral-300: oklch(87% 0 0);
--color-neutral-400: oklch(70.8% 0 0); --color-neutral-400: oklch(70.8% 0 0);
--color-neutral-500: oklch(55.6% 0 0); --color-neutral-500: oklch(55.6% 0 0);
--color-neutral-600: oklch(43.9% 0 0);
--color-neutral-700: oklch(37.1% 0 0); --color-neutral-700: oklch(37.1% 0 0);
--color-neutral-800: oklch(26.9% 0 0); --color-neutral-800: oklch(26.9% 0 0);
--color-neutral-900: oklch(20.5% 0 0);
--color-black: #000; --color-black: #000;
--color-white: #fff; --color-white: #fff;
--spacing: 0.25rem; --spacing: 0.25rem;
@@ -189,6 +191,24 @@
.\@container { .\@container {
container-type: inline-size; container-type: inline-size;
} }
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.top-2\.5 {
top: calc(var(--spacing) * 2.5);
}
.top-\[0\.125rem\] {
top: 0.125rem;
}
.right-1 {
right: calc(var(--spacing) * 1);
}
.right-2\.5 {
right: calc(var(--spacing) * 2.5);
}
.col-span-2 { .col-span-2 {
grid-column: span 2 / span 2; grid-column: span 2 / span 2;
} }
@@ -207,13 +227,19 @@
.mt-1 { .mt-1 {
margin-top: calc(var(--spacing) * 1); margin-top: calc(var(--spacing) * 1);
} }
.mb-1 {
margin-bottom: calc(var(--spacing) * 1);
}
.mb-2 { .mb-2 {
margin-bottom: calc(var(--spacing) * 2); margin-bottom: calc(var(--spacing) * 2);
} }
.icon-\[material-symbols-light--add-circle-outline\] { .ml-1 {
margin-left: calc(var(--spacing) * 1);
}
.icon-\[material-symbols-light--cancel-outline\] {
display: inline-block; display: inline-block;
width: 1.5em; width: 1.25em;
height: 1.5em; height: 1.25em;
background-color: currentColor; background-color: currentColor;
-webkit-mask-image: var(--svg); -webkit-mask-image: var(--svg);
mask-image: var(--svg); mask-image: var(--svg);
@@ -221,12 +247,12 @@
mask-repeat: no-repeat; mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%; -webkit-mask-size: 100% 100%;
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\] { .icon-\[material-symbols-light--check-circle-outline\] {
display: inline-block; display: inline-block;
width: 1.5em; width: 1.25em;
height: 1.5em; height: 1.25em;
background-color: currentColor; background-color: currentColor;
-webkit-mask-image: var(--svg); -webkit-mask-image: var(--svg);
mask-image: var(--svg); mask-image: var(--svg);
@@ -238,8 +264,8 @@
} }
.icon-\[material-symbols-light--circle-outline\] { .icon-\[material-symbols-light--circle-outline\] {
display: inline-block; display: inline-block;
width: 1.5em; width: 1.25em;
height: 1.5em; height: 1.25em;
background-color: currentColor; background-color: currentColor;
-webkit-mask-image: var(--svg); -webkit-mask-image: var(--svg);
mask-image: var(--svg); mask-image: var(--svg);
@@ -249,10 +275,23 @@
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='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"); --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\] { .icon-\[material-symbols-light--more-time\] {
display: inline-block; display: inline-block;
width: 1.5em; width: 1.25em;
height: 1.5em; height: 1.25em;
background-color: currentColor; background-color: currentColor;
-webkit-mask-image: var(--svg); -webkit-mask-image: var(--svg);
mask-image: var(--svg); mask-image: var(--svg);
@@ -264,8 +303,8 @@
} }
.icon-\[material-symbols-light--motion-photos-paused-outline\] { .icon-\[material-symbols-light--motion-photos-paused-outline\] {
display: inline-block; display: inline-block;
width: 1.5em; width: 1.25em;
height: 1.5em; height: 1.25em;
background-color: currentColor; background-color: currentColor;
-webkit-mask-image: var(--svg); -webkit-mask-image: var(--svg);
mask-image: var(--svg); mask-image: var(--svg);
@@ -275,6 +314,22 @@
mask-size: 100% 100%; mask-size: 100% 100%;
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9.808 14.616h1V9.385h-1zm3.384 0h1V9.385h-1zM12.003 21q-1.866 0-3.51-.705q-1.643-.706-2.859-1.915t-1.925-2.843T3 12.039q0-.905.167-1.778t.497-1.713l.78.78q-.219.65-.331 1.32T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.675 0-1.332.112t-1.3.332l-.776-.775q.789-.315 1.606-.492T11.885 3q1.887 0 3.546.701t2.894 1.926t1.955 2.866t.72 3.505t-.708 3.509t-1.924 2.859t-2.856 1.925t-3.509.709M5.923 6.808q-.356 0-.62-.265q-.264-.264-.264-.62t.264-.62t.62-.264t.62.264t.265.62t-.265.62t-.62.265M12 12'/%3E%3C/svg%3E"); --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9.808 14.616h1V9.385h-1zm3.384 0h1V9.385h-1zM12.003 21q-1.866 0-3.51-.705q-1.643-.706-2.859-1.915t-1.925-2.843T3 12.039q0-.905.167-1.778t.497-1.713l.78.78q-.219.65-.331 1.32T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.675 0-1.332.112t-1.3.332l-.776-.775q.789-.315 1.606-.492T11.885 3q1.887 0 3.546.701t2.894 1.926t1.955 2.866t.72 3.505t-.708 3.509t-1.924 2.859t-2.856 1.925t-3.509.709M5.923 6.808q-.356 0-.62-.265q-.264-.264-.264-.62t.264-.62t.62-.264t.62.264t.265.62t-.265.62t-.62.265M12 12'/%3E%3C/svg%3E");
} }
.icon-\[material-symbols-light--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 { .flex {
display: flex; display: flex;
} }
@@ -298,9 +353,19 @@
width: calc(var(--spacing) * 4); width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4); height: calc(var(--spacing) * 4);
} }
.size-5 {
width: calc(var(--spacing) * 5);
height: calc(var(--spacing) * 5);
}
.h-2 {
height: calc(var(--spacing) * 2);
}
.h-4 { .h-4 {
height: calc(var(--spacing) * 4); height: calc(var(--spacing) * 4);
} }
.h-5 {
height: calc(var(--spacing) * 5);
}
.h-8 { .h-8 {
height: calc(var(--spacing) * 8); height: calc(var(--spacing) * 8);
} }
@@ -316,6 +381,9 @@
.w-4 { .w-4 {
width: calc(var(--spacing) * 4); width: calc(var(--spacing) * 4);
} }
.w-5 {
width: calc(var(--spacing) * 5);
}
.w-9\/10 { .w-9\/10 {
width: calc(9/10 * 100%); width: calc(9/10 * 100%);
} }
@@ -334,21 +402,45 @@
.flex-grow { .flex-grow {
flex-grow: 1; flex-grow: 1;
} }
.grow { .grow-0 {
flex-grow: 1; flex-grow: 0;
} }
.grow-1 { .grow-1 {
flex-grow: 1; flex-grow: 1;
} }
.basis-\[content\] {
flex-basis: content;
}
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.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-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.grid-cols-5 { .grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
} }
.grid-cols-\[3fr_2fr_2fr_2fr_3fr_3fr_3fr\] {
grid-template-columns: 3fr 2fr 2fr 2fr 3fr 3fr 3fr;
}
.grid-cols-subgrid {
grid-template-columns: subgrid;
}
.grid-rows-6 {
grid-template-rows: repeat(6, minmax(0, 1fr));
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
@@ -364,6 +456,9 @@
.items-center { .items-center {
align-items: center; align-items: center;
} }
.items-end {
align-items: flex-end;
}
.justify-around { .justify-around {
justify-content: space-around; justify-content: space-around;
} }
@@ -396,30 +491,61 @@
border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
} }
} }
.divide-neutral-300 {
:where(& > :not(:last-child)) {
border-color: var(--color-neutral-300);
}
}
.justify-self-end { .justify-self-end {
justify-self: flex-end; justify-self: flex-end;
} }
.overflow-hidden { .overflow-hidden {
overflow: hidden; overflow: hidden;
} }
.rounded {
border-radius: 0.25rem;
}
.rounded-full { .rounded-full {
border-radius: calc(infinity * 1px); border-radius: calc(infinity * 1px);
} }
.rounded-md { .rounded-md {
border-radius: var(--radius-md); border-radius: var(--radius-md);
} }
.rounded-none {
border-radius: 0;
}
.border { .border {
border-style: var(--tw-border-style); border-style: var(--tw-border-style);
border-width: 1px; border-width: 1px;
} }
.border-neutral-200 { .border-0 {
border-color: var(--color-neutral-200); border-style: var(--tw-border-style);
border-width: 0px;
}
.border-r-0 {
border-right-style: var(--tw-border-style);
border-right-width: 0px;
}
.border-r-1 {
border-right-style: var(--tw-border-style);
border-right-width: 1px;
}
.border-b-0 {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 0px;
}
.border-dashed {
--tw-border-style: dashed;
border-style: dashed;
} }
.border-neutral-300 { .border-neutral-300 {
border-color: var(--color-neutral-300); border-color: var(--color-neutral-300);
} }
.border-neutral-900 { .border-neutral-500 {
border-color: var(--color-neutral-900); border-color: var(--color-neutral-500);
}
.border-neutral-600 {
border-color: var(--color-neutral-600);
} }
.bg-accent { .bg-accent {
background-color: var(--color-accent); background-color: var(--color-accent);
@@ -448,6 +574,9 @@
.p-2 { .p-2 {
padding: calc(var(--spacing) * 2); padding: calc(var(--spacing) * 2);
} }
.p-8 {
padding: calc(var(--spacing) * 8);
}
.px-3 { .px-3 {
padding-inline: calc(var(--spacing) * 3); padding-inline: calc(var(--spacing) * 3);
} }
@@ -482,6 +611,12 @@
.text-accent { .text-accent {
color: var(--color-accent); color: var(--color-accent);
} }
.text-black {
color: var(--color-black);
}
.text-neutral-300 {
color: var(--color-neutral-300);
}
.text-neutral-500 { .text-neutral-500 {
color: var(--color-neutral-500); color: var(--color-neutral-500);
} }
@@ -497,6 +632,9 @@
.text-red-600 { .text-red-600 {
color: var(--color-red-600); color: var(--color-red-600);
} }
.text-slate-700 {
color: var(--color-slate-700);
}
.uppercase { .uppercase {
text-transform: uppercase; text-transform: uppercase;
} }
@@ -517,6 +655,18 @@
--tw-duration: 300ms; --tw-duration: 300ms;
transition-duration: 300ms; transition-duration: 300ms;
} }
.\*\:text-center {
:is(& > *) {
text-align: center;
}
}
.\*\:not-print\:p-2 {
:is(& > *) {
@media not print {
padding: calc(var(--spacing) * 2);
}
}
}
.group-hover\:text-black { .group-hover\:text-black {
&:is(:where(.group):hover *) { &:is(:where(.group):hover *) {
@media (hover: hover) { @media (hover: hover) {
@@ -531,9 +681,9 @@
} }
} }
} }
.group-\[\.edit\]\:block { .group-\[\.edit\]\:ml-2 {
&:is(:where(.group):is(.edit) *) { &:is(:where(.group):is(.edit) *) {
display: block; margin-left: calc(var(--spacing) * 2);
} }
} }
.group-\[\.edit\]\:flex { .group-\[\.edit\]\:flex {
@@ -551,18 +701,21 @@
display: inline; display: inline;
} }
} }
.group-\[\.edit\]\/button\:block {
&:is(:where(.group\/button):is(.edit) *) {
display: block;
}
}
.group-\[\.edit\]\/button\:hidden {
&:is(:where(.group\/button):is(.edit) *) {
display: none;
}
}
.placeholder\:text-neutral-400 { .placeholder\:text-neutral-400 {
&::placeholder { &::placeholder {
color: var(--color-neutral-400); color: var(--color-neutral-400);
} }
} }
.hover\:border-neutral-300 {
&:hover {
@media (hover: hover) {
border-color: var(--color-neutral-300);
}
}
}
.hover\:border-neutral-500 { .hover\:border-neutral-500 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@@ -584,17 +737,10 @@
} }
} }
} }
.hover\:bg-red-600 { .hover\:bg-red-700 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
background-color: var(--color-red-600); background-color: var(--color-red-700);
}
}
}
.hover\:text-accent {
&:hover {
@media (hover: hover) {
color: var(--color-accent);
} }
} }
} }
@@ -605,11 +751,6 @@
} }
} }
} }
.focus\:border-neutral-400 {
&:focus {
border-color: var(--color-neutral-400);
}
}
.focus\:bg-neutral-700 { .focus\:bg-neutral-700 {
&:focus { &:focus {
background-color: var(--color-neutral-700); background-color: var(--color-neutral-700);
@@ -636,11 +777,6 @@
opacity: 50%; opacity: 50%;
} }
} }
.max-md\:flex {
@media (width < 48rem) {
display: flex;
}
}
.max-md\:grid { .max-md\:grid {
@media (width < 48rem) { @media (width < 48rem) {
display: grid; display: grid;
@@ -651,11 +787,6 @@
display: none; display: none;
} }
} }
.max-md\:flex-col {
@media (width < 48rem) {
flex-direction: column;
}
}
.max-md\:divide-y-1 { .max-md\:divide-y-1 {
@media (width < 48rem) { @media (width < 48rem) {
:where(& > :not(:last-child)) { :where(& > :not(:last-child)) {
@@ -728,8 +859,8 @@
color: transparent; color: transparent;
} }
} }
.group-\[\.edit\]\:md\:block { .group-\[\.edit\]\/button\:md\:block {
&:is(:where(.group):is(.edit) *) { &:is(:where(.group\/button):is(.edit) *) {
@media (width >= 48rem) { @media (width >= 48rem) {
display: block; display: block;
} }
@@ -755,6 +886,12 @@
} }
} }
} }
.lg\:border-0 {
@media (width >= 64rem) {
border-style: var(--tw-border-style);
border-width: 0px;
}
}
.\@7xl\:grid { .\@7xl\:grid {
@container (width >= 80rem) { @container (width >= 80rem) {
display: grid; display: grid;
@@ -771,10 +908,17 @@
} }
} }
} }
@layer base {
body {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
background-color: white;
}
}
@layer components { @layer components {
.grid-main { .grid-main {
display: grid; display: grid;
grid-template-columns: 2fr auto 1fr; grid-template-columns: 4fr 3fr 3fr 1fr;
align-items: stretch; align-items: stretch;
} }
.grid-sub { .grid-sub {
@@ -807,9 +951,12 @@
border-width: 1px; border-width: 1px;
border-color: var(--color-neutral-800); border-color: var(--color-neutral-800);
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-timing-function: var( --tw-ease, var(--default-transition-timing-function) );
transition-duration: var(--tw-duration, var(--default-transition-duration)); transition-duration: var(--tw-duration, var(--default-transition-duration));
} }
input.btn, select.btn {
transition-duration: 300ms;
}
.btn:hover { .btn:hover {
color: var(--color-white); color: var(--color-white);
background-color: var(--color-neutral-700); background-color: var(--color-neutral-700);
@@ -818,8 +965,38 @@
opacity: 50%; opacity: 50%;
pointer-events: none; pointer-events: none;
} }
.btn:active, .btn:focus { input.btn, select.btn {
background-color: var(--color-neutral-700); 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) { @media (width >=48rem) {
.grid-main { .grid-main {

View File

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

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.924 // templ: version: v0.3.943
package templates package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@@ -1,13 +1,7 @@
package templates package templates
import ( import "arbeitszeitmessung/models"
"arbeitszeitmessung/helper" import "arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"fmt"
"log"
"strconv"
"time"
)
templ Base() { templ Base() {
<!DOCTYPE html> <!DOCTYPE html>
@@ -19,27 +13,16 @@ templ Base() {
</head> </head>
} }
templ TimePage(workDays []models.WorkDay, lastSub time.Time) { templ LoginPage(success bool, errorMsg string) {
@Base()
@headerComponent()
<div class="grid-main divide-y-1">
@inputForm()
for _, day := range workDays {
@dayComponent(day, day.Day.Before(lastSub))
}
</div>
@LegendComponent()
}
templ LoginPage(failed bool) {
@Base() @Base()
<div class="w-full h-[100vh] flex flex-col justify-center items-center"> <div class="w-full h-[100vh] flex flex-col justify-center items-center">
<form method="POST" class="w-9/10 md:w-1/2 flex flex-col gap-4 p-2 mb-2"> <form method="POST" class="w-9/10 md:w-1/2 flex flex-col gap-4 p-2 mb-2">
<h1 class="font-bold uppercase text-xl text-center mb-2">Benutzer Anmelden</h1> <h1 class="font-bold uppercase text-xl text-center mb-2">Benutzer Anmelden</h1>
<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="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"/> <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 failed { if !success {
<p class="text-red-600 text-sm">Login fehlgeschlagen, bitte erneut versuchen!</p> <p class="text-red-600 text-sm">Login fehlgeschlagen, bitte erneut versuchen!</p>
<p class="text-red-600 text-sm">{ errorMsg }</p>
} }
<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> <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> </form>
@@ -47,6 +30,9 @@ templ LoginPage(failed bool) {
} }
templ UserPage(status int) { templ UserPage(status int) {
{{
user := ctx.Value("user").(models.User)
}}
@Base() @Base()
@headerComponent() @headerComponent()
<div class="grid-main divide-y-1"> <div class="grid-main divide-y-1">
@@ -69,6 +55,14 @@ templ UserPage(status int) {
<button name="action" value="change-pass" type="submit" class="btn">Ändern</button> <button name="action" value="change-pass" type="submit" class="btn">Ändern</button>
</div> </div>
</form> </form>
<div class="grid-sub responsive lg:divide-x-1">
<h1 class="grid-cell font-bold uppercase text-xl text-center">Nutzerdaten</h1>
<div class="grid-cell col-span-3">
<p>Nutzername: <span class="text-neutral-500">{ user.Vorname } { user.Name }</span></p>
<p>Personalnummer: <span class="text-neutral-500">{ user.PersonalNummer }</span></p>
</div>
<div></div>
</div>
<div class="grid-sub responsive lg:divide-x-1"> <div class="grid-sub responsive lg:divide-x-1">
<h1 class="grid-cell font-bold uppercase text-xl text-center">Nutzer abmelden</h1> <h1 class="grid-cell font-bold uppercase text-xl text-center">Nutzer abmelden</h1>
<div class="grid-cell col-span-3"> <div class="grid-cell col-span-3">
@@ -92,100 +86,57 @@ templ statusCheckMark(status models.WeekStatus, target models.WeekStatus) {
templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) { templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) {
@Base() @Base()
@headerComponent() @headerComponent()
{{
progress := (float32(userWeek.Worktime.Hours()) / userWeek.User.ArbeitszeitPerWoche) * 100
log.Println(userWeek.CheckStatus())
}}
<div class="grid-main divide-y-1"> <div class="grid-main divide-y-1">
<div class="grid-sub lg:divide-x-1 responsive @container"> <div class="grid-sub lg:divide-x-1 max-md:divide-y-1 responsive @container">
<div class="grid-cell col-span-full bg-neutral-300"> <div class="grid-cell col-span-full bg-neutral-300 lg:border-0">
<h2 class="text-2xl uppercase font-bold">Eigene Abrechnung</h2> <h2 class="text-2xl uppercase font-bold">Eigene Abrechnung</h2>
</div> </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>
</div>
</div> </div>
@workWeekComponent(userWeek, false)
if len(weeks) > 0 { if len(weeks) > 0 {
<div class="grid-cell col-span-full bg-neutral-300"> <div class="grid-cell col-span-full bg-neutral-300">
<h2 class="text-2xl uppercase font-bold">Abrechnung Mitarbeiter</h2> <h2 class="text-2xl uppercase font-bold">Abrechnung Mitarbeiter</h2>
</div> </div>
} }
for _, week := range weeks { for _, week := range weeks {
@employeComponent(week) @workWeekComponent(week, true)
} }
</div> </div>
} }
templ TeamPresencePage(teamPresence map[bool][]models.User) { templ TeamPresencePage(teamPresence map[models.User]bool) {
@Base() @Base()
@headerComponent() @headerComponent()
<div class="grid-main divide-y-1"> <div class="grid-main divide-y-1">
<div class="grid-sub divide-x-1"> <div class="grid-sub divide-x-1">
<h2 class="grid-cell font-bold uppercase">Anwesend</h2> <h2 class="grid-cell font-bold uppercase">Mitarbeiter</h2>
<div class="flex flex-col col-span-2 md:col-span-4"> </div>
for _, user := range teamPresence[true] { for user, present := range teamPresence {
@userPresenceComponent(user, true) <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> </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 class="grid-sub divide-x-1">
</div> // <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> </div>
} }
templ LogoutButton() { templ LogoutButton() {
<button onclick="logoutUser()" type="button" class="">Abmelden</button> <button onclick="logoutUser()" type="button" class="cursor-pointer">Abmelden</button>
} }

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.924 // templ: version: v0.3.943
package templates package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -8,14 +8,8 @@ package templates
import "github.com/a-h/templ" import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime" import templruntime "github.com/a-h/templ/runtime"
import ( import "arbeitszeitmessung/models"
"arbeitszeitmessung/helper" import "arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"fmt"
"log"
"strconv"
"time"
)
func Base() templ.Component { func Base() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
@@ -46,7 +40,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) { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -71,72 +65,30 @@ func TimePage(workDays []models.WorkDay, lastSub time.Time) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"grid-main divide-y-1\">") if !success {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<p class=\"text-red-600 text-sm\">Login fehlgeschlagen, bitte erneut versuchen!</p><p class=\"text-red-600 text-sm\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = inputForm().Render(ctx, templ_7745c5c3_Buffer) var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errorMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 25, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, day := range workDays { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p>")
templ_7745c5c3_Err = dayComponent(day, day.Day.Before(lastSub)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</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
}
templ_7745c5c3_Err = LegendComponent().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func LoginPage(failed 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_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, 4, "<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 failed {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"text-red-600 text-sm\">Login fehlgeschlagen, bitte erneut versuchen!</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -165,6 +117,8 @@ func UserPage(status int) templ.Component {
templ_7745c5c3_Var4 = templ.NopComponent templ_7745c5c3_Var4 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
user := ctx.Value("user").(models.User)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
@@ -173,28 +127,67 @@ func UserPage(status int) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
switch { switch {
case status == 401: case status == 401:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
case status >= 400: case status >= 400:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
case status == 202: case status == 202:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</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: 61, 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: 61, 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: 62, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span></p></div><div></div></div><div class=\"grid-sub responsive lg:divide-x-1\"><h1 class=\"grid-cell font-bold uppercase text-xl text-center\">Nutzer abmelden</h1><div class=\"grid-cell col-span-3\"><p>Nutzer von Weboberfläche abmelden.</p></div><div class=\"grid-cell\"><button onclick=\"logoutUser\" type=\"button\" class=\"btn\">Abmelden</button></div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -218,18 +211,18 @@ func statusCheckMark(status models.WeekStatus, target models.WeekStatus) templ.C
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx) templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil { if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var5 = templ.NopComponent templ_7745c5c3_Var8 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
if status >= target { if status >= target {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"icon-[material-symbols-light--check-circle-outline]\"></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"icon-[material-symbols-light--check-circle-outline]\"></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"icon-[material-symbols-light--circle-outline]\"></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"icon-[material-symbols-light--circle-outline]\"></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -254,9 +247,9 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx) templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil { if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var6 = templ.NopComponent templ_7745c5c3_Var9 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
@@ -267,182 +260,27 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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-2xl uppercase font-bold\">Eigene Abrechnung</h2></div></div>")
progress := (float32(userWeek.Worktime.Hours()) / userWeek.User.ArbeitszeitPerWoche) * 100
log.Println(userWeek.CheckStatus())
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"grid-main divide-y-1\"><div class=\"grid-sub lg:divide-x-1 responsive @container\"><div class=\"grid-cell col-span-full bg-neutral-300\"><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\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = weekPicker(userWeek.WeekStart).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = workWeekComponent(userWeek, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div><h2 class=\"uppercase font-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, 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: 108, Col: 101}
}
_, 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, 16, "</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, 17, "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, 18, "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, 19, "<div><p>Arbeitszeit: ")
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", helper.FormatDuration(userWeek.Worktime)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 123, Col: 84}
}
_, 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, 20, "</p><p>Überstunden: ")
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.Overtime)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 124, Col: 85}
}
_, 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, 21, "</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, 22, "</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, 23, "</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_Var10 string
templ_7745c5c3_Var10, 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: 140, Col: 88}
}
_, 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, "\"> <input type=\"hidden\" name=\"week\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, 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: 141, Col: 86}
}
_, 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, 25, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
switch userWeek.CheckStatus() {
case models.WeekStatusNone:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<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, 27, "<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, 28, "<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, 29, "<button")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if userWeek.Status < models.WeekStatusSent {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " 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, 32, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " type=\"submit\" class=\"btn\">Senden</button></form></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(weeks) > 0 { if len(weeks) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<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-2xl uppercase font-bold\">Abrechnung Mitarbeiter</h2></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
for _, week := range weeks { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -450,7 +288,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component
}) })
} }
func TeamPresencePage(teamPresence map[bool][]models.User) templ.Component { func TeamPresencePage(teamPresence map[models.User]bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -466,9 +304,9 @@ func TeamPresencePage(teamPresence map[bool][]models.User) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var12 := templ.GetChildren(ctx) templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var12 == nil { if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var12 = templ.NopComponent templ_7745c5c3_Var10 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer)
@@ -479,27 +317,66 @@ func TeamPresencePage(teamPresence map[bool][]models.User) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<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\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"grid-main divide-y-1\"><div class=\"grid-sub divide-x-1\"><h2 class=\"grid-cell font-bold uppercase\">Mitarbeiter</h2></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, user := range teamPresence[true] { for user, present := range teamPresence {
templ_7745c5c3_Err = userPresenceComponent(user, true).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<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, 21, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 118, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 118, Col: 36}
}
_, 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, 23, "</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, 24, "<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, 25, "<span class=\"text-neutral-500\">Abwesend</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</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\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div></div>")
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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</div></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -528,7 +405,7 @@ func LogoutButton() templ.Component {
templ_7745c5c3_Var13 = templ.NopComponent templ_7745c5c3_Var13 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<button onclick=\"logoutUser()\" type=\"button\" class=\"\">Abmelden</button>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<button onclick=\"logoutUser()\" type=\"button\" class=\"cursor-pointer\">Abmelden</button>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -0,0 +1,77 @@
package templates
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"time"
)
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() {
<p class="col-span-full">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,390 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.943
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models"
"time"
)
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_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = 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, 1, "<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_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vorname)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 17, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 17, Col: 56}
}
_, 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, 3, "</h1><p>Zeitraum: <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, 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: 18, Col: 52}
}
_, 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, 4, "</span> - <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(tsEnd.Format("02.01.2006"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 18, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span></p><p>Arbeitszeit: <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, 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: 19, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span></p><p>Überstunden: <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, 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: 20, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</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_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(kw)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 23, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</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_Var9 = []any{noBorder}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, 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: 36, Col: 59}
}
_, 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, 11, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 = []any{"grid grid-cols-subgrid col-span-3 " + noBorder}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
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, 14, "<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI].Timestamp.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 43, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI+1].Timestamp.Format("15:04"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 44, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI].BookingType.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 45, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if workDay.IsKurzArbeit() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<p class=\"col-span-full\">Kurzarbeit</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
absentDay, _ := day.(*models.Absence)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<p class=\"col-span-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(absentDay.AbwesenheitTyp.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 54, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</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, 23, " ")
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, 24, " ")
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, 25, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if day.Date().Weekday() == time.Friday {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<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, 27, "</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_Var18 := templ.GetChildren(ctx)
if templ_7745c5c3_Var18 == nil {
templ_7745c5c3_Var18 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
color := ""
if d.Abs() < time.Minute {
color = "text-neutral-300"
}
var templ_7745c5c3_Var19 = []any{color + " " + classes}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var19).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_Var20))
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
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, 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: 76, Col: 72}
}
_, 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, 30, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -4,7 +4,6 @@ import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"fmt" "fmt"
"log"
"strconv" "strconv"
"time" "time"
) )
@@ -27,77 +26,141 @@ templ weekPicker(weekStart time.Time) {
</svg> </svg>
</button> </button>
</form> </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) { templ defaultWeekDayComponent(u models.User, day models.IWorkDay) {
{{ work, pause := day.GetWorkTimeString() }}
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
@timeGaugeComponent(day.GetWorkDayProgress(user), false, false) @timeGaugeComponent(day.GetDayProgress(u), false)
<div class="flex flex-col"> <div class="flex flex-col">
<p class=""><span class="font-bold uppercase hidden md:inline">{ day.Day.Format("Mon") }:</span> { day.Day.Format("02.01.2006") }</p> <p class=""><span class="font-bold uppercase hidden md:inline">{ day.Date().Format("Mon") }:</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"> <div class="flex flex-row gap-2">
<span class="text-accent">{ work }</span> <span class="text-accent">{ helper.FormatDuration(work) }</span>
<span class="text-neutral-500">{ pause }</span> <span class="text-neutral-500">{ helper.FormatDuration(pause) }</span>
</div> </div>
<div class="flex flex-row gap-2 items-center"> <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"> <span class="icon-[material-symbols-light--schedule-outline] flex-shrink-0"></span>
<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> switch {
<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> case !workDay.TimeFrom.Equal(workDay.TimeTo):
</svg> <span>{ workDay.TimeFrom.Format("15:04") }</span>
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>-</span>
<span>{ day.TimeTo.Format("15:04") }</span> <span>{ workDay.TimeTo.Format("15:04") }</span>
} else { default:
<p>Keine Anwesenheit</p> <p>Keine Anwesenheit</p>
} }
</div> </div>
} else {
<p class="text-red-600">Bitte anpassen</p>
}
} else {
{{
absentDay, _ := day.(*models.Absence)
}}
<div>{ absentDay.AbwesenheitTyp.Name } </div>
}
</div> </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() year, kw := week.WeekStart.ISOWeek()
progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100 progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100
log.Println(progress)
}} }}
<div class="employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1 @container"> <div class="employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1 @container">
<div class="grid-cell flex flex-col max-md:bg-neutral-300 gap-2"> <div class="grid-cell flex flex-col max-md:bg-neutral-300 gap-2">
if !onlyAccept {
<div class="lg:hidden">
@weekPicker(week.WeekStart)
</div>
}
<p class="font-bold uppercase">{ week.User.Vorname } { week.User.Name }</p> <p class="font-bold uppercase">{ week.User.Vorname } { week.User.Name }</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"> <div class="flex flex-row gap-2 col-span-3">
@timeGaugeComponent(uint8(progress), false, false) @timeGaugeComponent(int8(progress), false)
<div> <div>
<p>Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDuration(week.Worktime)) }</p> <p>Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDuration(week.Worktime)) }</p>
<p>Überstunden: { fmt.Sprintf("%s", helper.FormatDuration(week.Overtime)) }</p> <p>Überstunden: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true)) }</p>
</div> </div>
</div> </div>
</div> </div>
<div class="grid-cell col-span-3 flex flex-col @7xl:grid @7xl:grid-cols-5 gap-2 content-baseline"> </div>
for _, day := range week.WorkDays { <div class="grid-cell col-span-3 flex flex-col @7xl:grid @7xl:grid-cols-5 gap-2 py-4 content-baseline">
@weekDayComponent(week.User, day) for _, day := range week.Days {
@defaultWeekDayComponent(week.User, day)
} }
</div> </div>
<form class="grid-cell flex flex-col justify-between gap-2" method="post"> <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> <p class="text-sm"><span class="">Woche:</span> { fmt.Sprintf("%02d-%d", kw, year) }</p>
<input type="hidden" name="method" value="accept"/> } 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="user" value={ strconv.Itoa(week.User.PersonalNummer) }/>
<input type="hidden" name="week" value={ week.WeekStart.Format(time.DateOnly) }/> <input type="hidden" name="week" value={ week.WeekStart.Format(time.DateOnly) }/>
<div class="flex flex-col gap-2"> if onlyAccept {
if week.Status == models.WeekStatusDifferences { if week.Status == models.WeekStatusDifferences {
<p class="text-red-600 text-sm">Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen</p> <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> <button type="submit" disabled?={ week.Status == models.WeekStatusDifferences } class="btn">Bestätigen</button>
// TODO maybe delete function } else {
// <button type="button" disabled?={ week.Status < models.WeekStatusDifferences } class="hover:bg-red-600 btn">Antrag löschen</button> switch {
</div> 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>
}
</form> </form>
</div> </div>
</div>
} }
templ userPresenceComponent(user models.User, present bool) { templ userPresenceComponent(user models.User, present bool) {

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.924 // templ: version: v0.3.943
package templates package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -12,7 +12,6 @@ import (
"arbeitszeitmessung/helper" "arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"fmt" "fmt"
"log"
"strconv" "strconv"
"time" "time"
) )
@@ -47,7 +46,7 @@ func weekPicker(weekStart time.Time) templ.Component {
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(weekStart.Format(time.DateOnly)) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(weekStart.Format(time.DateOnly))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 17, Col: 98} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 16, Col: 98}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -77,7 +76,7 @@ func weekPicker(weekStart time.Time) templ.Component {
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d, %d", kw, year)) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d, %d", kw, year))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 23, Col: 69} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 22, Col: 69}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -114,11 +113,176 @@ func weekPicker(weekStart time.Time) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if time.Since(weekStart) < 24*7*time.Hour { return nil
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>") })
}
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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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(day.Date().Format("Mon"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 92}
}
_, 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: 136}
}
_, 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 return nil
}) })
@@ -140,132 +304,18 @@ func weekDayComponent(user models.User, day models.WorkDay) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx) templ_7745c5c3_Var14 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil { if templ_7745c5c3_Var14 == nil {
templ_7745c5c3_Var6 = templ.NopComponent templ_7745c5c3_Var14 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
work, pause := day.GetWorkTimeString() templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div class=\"flex flex-row gap-2\"><div class=\"flex flex-col\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"flex flex-row gap-2\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = timeGaugeComponent(day.GetWorkDayProgress(user), false, false).Render(ctx, templ_7745c5c3_Buffer) if !day.RequiresAction() {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"flex flex-col\"><p class=\"\"><span class=\"font-bold uppercase hidden md:inline\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div></div>")
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: 40, 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: 40, 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: 42, 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: 43, 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: 51, 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: 53, 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: 55, 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>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -273,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) { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -289,145 +339,292 @@ func employeComponent(week models.WorkWeek) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var14 := templ.GetChildren(ctx) templ_7745c5c3_Var15 := templ.GetChildren(ctx)
if templ_7745c5c3_Var14 == nil { if templ_7745c5c3_Var15 == nil {
templ_7745c5c3_Var14 = templ.NopComponent templ_7745c5c3_Var15 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
year, kw := week.WeekStart.ISOWeek() year, kw := week.WeekStart.ISOWeek()
progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100 progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100
log.Println(progress) templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1 @container\"><div class=\"grid-cell flex flex-col max-md:bg-neutral-300 gap-2\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 string if !onlyAccept {
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Vorname) templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"lg:hidden\">")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 72, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ") 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 = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<p class=\"font-bold uppercase\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Name) templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Vorname)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 72, 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</p><div class=\"flex flex-row gap-2 col-span-3\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " ")
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: ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDuration(week.Worktime))) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 76, 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var18 string var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(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 { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 77, 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</p><p>Überstunden: ")
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> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var19 string 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 { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 87, 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</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
}
for _, day := range week.Days {
templ_7745c5c3_Err = defaultWeekDayComponent(week.User, day).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</div><div class=\"grid-cell flex flex-col gap-2 justify-between\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if onlyAccept {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<p class=\"text-sm\"><span class=\"\">Woche:</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var20 string var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(week.User.PersonalNummer)) templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d-%d", kw, year))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 89, Col: 82} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"> <input type=\"hidden\" name=\"week\" value=\"") 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var21 string var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(week.WeekStart.Format(time.DateOnly)) templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(method)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 90, 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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
}
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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if week.Status == models.WeekStatusDifferences { 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>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " disabled")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<button type=\"submit\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " class=\"btn\">Bestätigen</button>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if week.Status == models.WeekStatusDifferences { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " disabled") 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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, 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, 65, "</form></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -451,53 +648,53 @@ func userPresenceComponent(user models.User, present bool) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var22 := templ.GetChildren(ctx) templ_7745c5c3_Var24 := templ.GetChildren(ctx)
if templ_7745c5c3_Var22 == nil { if templ_7745c5c3_Var24 == nil {
templ_7745c5c3_Var22 = templ.NopComponent templ_7745c5c3_Var24 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if present { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var23 string var templ_7745c5c3_Var25 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname) templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 110, 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var24 string var templ_7745c5c3_Var26 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 110, 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -1,113 +1,41 @@
package templates package templates
import ( import (
"arbeitszeitmessung/helper"
"arbeitszeitmessung/models" "arbeitszeitmessung/models"
"fmt" "fmt"
"net/url"
"strconv" "strconv"
"time" "time"
) )
templ inputForm() { templ lineComponent() {
{{ <div class="flex flex-col w-2 py-2 items-center text-accent print:hidden">
urlParams := ctx.Value("urlParams").(url.Values) <svg class="size-2" viewBox="0 0 24 24" fill="currentColor">
user := ctx.Value("user").(models.User) <polygon points="12,2 22,12 12,22 2,12"></polygon>
}} </svg>
<div class="grid-sub divide-x-1 bg-neutral-300 max-md:flex max-md:flex-col"> <div class="w-[2px] bg-accent flex-grow -my-1"></div>
<div class="grid-cell md:col-span-1 max-md:grid grid-cols-2"> <svg class="size-2" viewBox="0 0 24 24" fill="currentColor">
<p class="font-bold uppercase">{ user.Vorname + " " + user.Name }</p> <polygon points="12,2 22,12 12,22 2,12"></polygon>
<div class="justify-self-end"> </svg>
<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>
</div> </div>
} }
templ dayComponent(workDay models.WorkDay, submitted bool) { templ changeButtonComponent(id string, workDay bool) {
{{ <button class="change-button-component btn w-auto group/button" type="button" onclick={ templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), id, workDay) }>
work, pause := workDay.GetWorkTimeString() <p class="hidden md:block group-[.edit]/button:hidden">Ändern</p>
user := ctx.Value("user").(models.User) <p class="hidden group-[.edit]/button:md:block">Absenden</p>
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>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4 md:hidden"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4 md:hidden">
<path class="group-[.edit]:hidden md:hidden" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"></path> <path class="group-[.edit]/button:hidden md:hidden" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"></path>
<path class="hidden group-[.edit]:block md:hidden" d="M12.736 3.97a.733.733 0 0 1 j1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425z"></path> <path class="hidden group-[.edit]/button:block md:hidden" d="M12.736 3.97a.733.733 0 0 1 j1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425z"></path>
</svg> </svg>
</button> </button>
<button class="hidden group-[.edit]:flex btn basis-[content] items-center" onclick={ templ.JSFuncCall("clearEditState") }><span class="size-5 icon-[material-symbols-light--cancel-outline]"></span></button>
} }
templ timeGaugeComponent(progress uint8, today bool, warning bool) { templ timeGaugeComponent(progress int8, today bool) {
{{ {{
var bgColor string var bgColor string
switch { switch {
case (warning): case (0 > progress):
bgColor = "bg-red-600" bgColor = "bg-red-600"
break break
case (progress > 0 && progress < 95): case (progress > 0 && progress < 95):
@@ -133,47 +61,63 @@ templ timeGaugeComponent(progress uint8, today bool, warning bool) {
} }
} }
templ lineComponent() { templ newAbsenceComponent() {
<div class="flex flex-col w-2 py-2 items-center text-accent print:hidden"> <div class="no-booking-component hidden group-[.edit]:flex flex-col gap-2 align-center ">
<svg class="size-2" viewBox="0 0 24 24" fill="currentColor"> <button type="button" name="absence" onclick={ templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), 0, false) } class="btn border-neutral-500">
<polygon points="12,2 22,12 12,22 2,12"></polygon> Neue Abwesenheit
</svg> </button>
<div class="w-[2px] bg-accent flex-grow -my-1"></div>
<svg class="size-2" viewBox="0 0 24 24" fill="currentColor">
<polygon points="12,2 22,12 12,22 2,12"></polygon>
</svg>
</div> </div>
} }
templ absenceComponent(d models.WorkDay) { templ absenceComponent(a *models.Absence, isKurzarbeit bool) {
<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> editBox := ""
<option value="0">Abwesenheit?</option> if isKurzarbeit {
for _, absence := range models.GetAbsenceTypesCached() { editBox = "edit-box"
<option value={ strconv.Itoa(int(absence.Id)) }>{ absence.Name }</option> }
}}
<div class={ "flex flex-row items-center gap-2", editBox }>
<input type="hidden" name="date_from" value={ a.DateFrom.Format("2006-01-02") }/>
<input type="hidden" name="date_to" value={ a.DateTo.Format("2006-01-02") }/>
<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>
}
</p>
<div class="w-full"></div>
if isKurzarbeit {
<button onclick={ templ.JSFuncCall("editAbsence", templ.JSExpression("this"), templ.JSExpression("event"), "time-"+a.Date().Format("2006-01-02")) } class="hidden btn border-0 rounded-none grow-0 w-auto group-[.edit]:inline">Bearbeiten</button>
} }
</select>
</div> </div>
} }
templ newBookingComponent(d models.WorkDay) { templ newBookingComponent(d *models.WorkDay) {
<div class="new-booking-component hidden group-[.edit]:flex flex-row gap-2 items-center"> <div class="new-booking-component hidden group-[.edit]:flex flex-row gap-2 items-center edit-box border-dashed">
<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 px-3 py-2 cursor-pointer"/>
<input name="timestamp" type="time" value={ time.Now().Format("15:04") } class="text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm 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") }/> <input name="date" type="hidden" value={ d.Day.Format("2006-01-02") }/>
<select name="check_in_out"> <div class="relative">
<select class="cursor-pointer appearance-none" name="check_in_out">
<option value="0" disabled>Kommen/Gehen</option> <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="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> <option value="4" selected?={ len(d.Bookings) > 0 && d.Bookings[len(d.Bookings)-1].CheckInOut%2 == 1 }>Gehen</option>
</select> </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> </div>
} }
templ bookingComponent(booking models.Booking) { templ bookingComponent(booking models.Booking) {
<div> <div>
<p class="text-neutral-500"> <p class="text-neutral-500 edit-box">
<span class="text-neutral-700 group-[.edit]:hidden inline">{ booking.Timestamp.Format("15:04") }</span> <span class="text-black group-[.edit]:hidden inline">{ booking.Timestamp.Format("15:04") }</span>
<input disabled name={ "booking_" + strconv.Itoa(booking.CounterId) } type="time" value={ booking.Timestamp.Format("15:04") } class="text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm 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 disabled name={ "booking_" + strconv.Itoa(booking.CounterId) } type="time" value={ booking.Timestamp.Format("15:04") } class="text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer"/>
{ booking.GetBookingType() } { booking.GetBookingType() }
</p> </p>
</div> </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">{ day.Date().Format("Mon") }:</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("2006-01-02") } 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("2006-01-02"), day.IsWorkDay())
</div>
</div>
}
templ absentInput(a models.Absence) {
<input type="hidden" name="date_from" value={ a.DateFrom.Format("2006-01-02") }/>
<input type="hidden" name="date_to" value={ a.DateTo.Format("2006-01-02") }/>
<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.943
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"arbeitszeitmessung/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(day.Date().Format("Mon"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 82}
}
_, 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: 126}
}
_, 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("2006-01-02"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 130, Col: 55}
}
_, 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("2006-01-02"), 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("2006-01-02"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 162, Col: 78}
}
_, 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("2006-01-02"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 163, Col: 74}
}
_, 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

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

@@ -4,11 +4,11 @@
DROP TABLE IF EXISTS "anwesenheit"; DROP TABLE IF EXISTS "anwesenheit";
CREATE TABLE "anwesenheit" ( CREATE TABLE "anwesenheit" (
"counter_id" bigserial NOT NULL, "counter_id" bigserial NOT NULL,
"timestamp" timestamptz NULL DEFAULT CURRENT_TIMESTAMP, "timestamp" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
"card_uid" character varying(255) NOT NULL, "card_uid" character varying(255) NOT NULL,
"check_in_out" smallint NOT NULL, "check_in_out" smallint NOT NULL,
"geraet_id" smallint NULL, "geraet_id" smallint NOT NULL,
"anwesenheit_typ" int2, "anwesenheit_typ" int2 NOT NULL,
PRIMARY KEY ("counter_id") PRIMARY KEY ("counter_id")
); );
@@ -32,8 +32,8 @@ DROP TABLE IF EXISTS "s_personal_daten";
CREATE TABLE "s_personal_daten" ( CREATE TABLE "s_personal_daten" (
"personal_nummer" int4 NOT NULL PRIMARY KEY, "personal_nummer" int4 NOT NULL PRIMARY KEY,
"aktiv_beschaeftigt" bool, "aktiv_beschaeftigt" bool,
"vorname" varchar(255), "vorname" varchar(255) NOT NULL,
"nachname" varchar(255), "nachname" varchar(255) NOT NULL,
"geburtsdatum" date, "geburtsdatum" date,
"plz" varchar(255), "plz" varchar(255),
"adresse" varchar(255), "adresse" varchar(255),
@@ -81,8 +81,8 @@ CREATE TABLE "wochen_report" (
"personal_nummer" int4, "personal_nummer" int4,
"woche_start" date, "woche_start" date,
"bestaetigt" bool DEFAULT FALSE, "bestaetigt" bool DEFAULT FALSE,
"arbeitszeit" float4, "arbeitszeit" interval,
"ueberstunden" float4, "ueberstunden" interval,
UNIQUE ("personal_nummer", "woche_start") UNIQUE ("personal_nummer", "woche_start")
); );
@@ -91,10 +91,11 @@ CREATE TABLE "abwesenheit" (
"counter_id" bigserial PRIMARY KEY, "counter_id" bigserial PRIMARY KEY,
"card_uid" varchar(255), "card_uid" varchar(255),
"abwesenheit_typ" int2, "abwesenheit_typ" int2,
"datum" timestamptz(6) DEFAULT NOW()::DATE "datum_from" timestamptz DEFAULT NOW()::DATE,
"datum_to" timestamptz
); );
DROP TABLE IF EXISTS "s_abwesenheit_typen"; DROP TABLE IF EXISTS "s_abwesenheit_typen";
CREATE TABLE "s_abwesenheit_typen" ( CREATE TABLE "s_abwesenheit_typen" (
"abwesenheit_id" int2 PRIMARY KEY, "abwesenheit_id" int2 PRIMARY KEY,
"abwesenheit_name" varchar(255), "abwesenheit_name" varchar(255),

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 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, '07:00:00', '20:00:00', 0); (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 INSERT INTO "user_password" ("personal_nummer", "pass_hash") VALUES
(123, crypt('max_pass', gen_salt('bf'))); (123, crypt('max_pass', gen_salt('bf')));

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

View File

@@ -30,8 +30,11 @@ services:
NO_CORS: true NO_CORS: true
ports: ports:
- ${EXPOSED_PORT}:8080 - ${EXPOSED_PORT}:8080
volumes:
- ../logs:/app/Backend/logs
depends_on: depends_on:
- db - db
swagger: swagger:
image: swaggerapi/swagger-ui image: swaggerapi/swagger-ui
restart: unless-stopped restart: unless-stopped

View File

@@ -28,4 +28,6 @@ services:
- ${EXPOSED_PORT}:8080 - ${EXPOSED_PORT}:8080
depends_on: depends_on:
- db - db
volumes:
- ../logs:/app/Backend/logs
restart: unless-stopped restart: unless-stopped

674
LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -44,8 +44,8 @@ generateFrontend:
backend: generateFrontend login_registry backend: generateFrontend login_registry
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:latest Backend --push docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:latest Backend --load #--push
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:${GIT_COMMIT} Backend --push # docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:${GIT_COMMIT} Backend //--push
test: test:
$(MAKE) -C Backend test $(MAKE) -C Backend test

View File

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

10
db.sql
View File

@@ -198,12 +198,12 @@ ORDER BY d.work_date;
WITH params AS ( WITH params AS (
SELECT SELECT
'acde-edca'::varchar AS card_uid, 'aaaa-aaaa'::varchar AS card_uid,
101::int AS geraet_id, 101::int AS geraet_id,
14::int AS start_days_ago, -- how many days back to start 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::int AS end_days_ahead, -- how many days forward (0 = today)
0.5::float AS pause_probability, 0::float AS pause_probability,
0.2::float AS absence_probability 0.0::float AS absence_probability
), ),
days AS ( days AS (
SELECT gs::date AS work_date, p.card_uid, p.geraet_id, p.pause_probability, p.absence_probability SELECT gs::date AS work_date, p.card_uid, p.geraet_id, p.pause_probability, p.absence_probability
@@ -249,8 +249,8 @@ all_bookings AS (
SELECT * FROM pause_bookings SELECT * FROM pause_bookings
), ),
ins_anw AS ( ins_anw AS (
INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id) INSERT INTO anwesenheit ("timestamp", "card_uid", "check_in_out", "geraet_id", "anwesenheit_typ")
SELECT ts, card_uid, check_in_out, geraet_id SELECT ts, card_uid, check_in_out, geraet_id, 1 as anwesenheit_typ
FROM all_bookings FROM all_bookings
WHERE ts <= NOW() WHERE ts <= NOW()
ORDER BY work_date, ts ORDER BY work_date, ts

View File

@@ -0,0 +1,4 @@
-- reverse: modify "s_personal_daten" table
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

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

View File

@@ -0,0 +1,7 @@
ALTER TABLE wochen_report
ALTER COLUMN ueberstunden TYPE float4
USING
extract(epoch from ueberstunden) / 3600.0,
ALTER COLUMN arbeitszeit TYPE float4
USING
extract(epoch from arbeitszeit) / 3600.0;

View File

@@ -0,0 +1,19 @@
ALTER TABLE wochen_report
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

@@ -1,4 +1,4 @@
h1:hyA7xJMv355hJfvtzD9tKSrkAEtsQ/vVixDZDDBxoh0= h1:3AxgD8mnu/F+JGtJu9FZvA9Ro0UUtGPgyjskKtfTYUQ=
20250901201159_initial.down.sql h1:cmF5CvNGqEfcmbRgiqaqDWERdNNRaMzarbNLJ/Y35o4= 20250901201159_initial.down.sql h1:cmF5CvNGqEfcmbRgiqaqDWERdNNRaMzarbNLJ/Y35o4=
20250901201159_initial.up.sql h1:Yrak/+wfQ4Tu/dVR/cUZ/75DlAcv4G/OJXDqpgSw47U= 20250901201159_initial.up.sql h1:Yrak/+wfQ4Tu/dVR/cUZ/75DlAcv4G/OJXDqpgSw47U=
20250901201250_control_tables.down.sql h1:f/KmhO9pOI45J8ZRjFonvD3CypB+rOoGOPN2WMFHvOw= 20250901201250_control_tables.down.sql h1:f/KmhO9pOI45J8ZRjFonvD3CypB+rOoGOPN2WMFHvOw=
@@ -7,3 +7,9 @@ h1:hyA7xJMv355hJfvtzD9tKSrkAEtsQ/vVixDZDDBxoh0=
20250901201710_triggers_extension.up.sql h1:nUBPd2eDssi/TwMVF/nOJkIM5rUM0iINdg1K9pZRZN0= 20250901201710_triggers_extension.up.sql h1:nUBPd2eDssi/TwMVF/nOJkIM5rUM0iINdg1K9pZRZN0=
20250903221313_overtime.down.sql h1:X+jJESqcZ6ZTd2H563z6kRaXb4dn4sA02D3ck2795v8= 20250903221313_overtime.down.sql h1:X+jJESqcZ6ZTd2H563z6kRaXb4dn4sA02D3ck2795v8=
20250903221313_overtime.up.sql h1:C3DSiNVpe9v0Un1DEQ0lsy5yToR8iqcggv91GSr6tRE= 20250903221313_overtime.up.sql h1:C3DSiNVpe9v0Un1DEQ0lsy5yToR8iqcggv91GSr6tRE=
20250903233030_non_null_contraints.down.sql h1:42TZzPsji2Ze50k6sLwgIuNo4Trk3m3ni/aIfQJ97dE=
20250903233030_non_null_contraints.up.sql h1:k6zR5YNSAP4fo5QEc58KZ0LxvEz1nl0X/AAcZ+TG3I4=
20250904114004_intervals.down.sql h1:SquJAPinzFIRN6fJjLLIRsz59Tyr4RwGiGuOFI/N1SQ=
20250904114004_intervals.up.sql h1:AFqncTGOiEZVBbhWFqN2zlQ7DyhybB5wJr6a36Atk1E=
20250916093608_kurzarbeit.down.sql h1:ljM1a1pQCxOQiXRaXU04GC4V9yy2y20x5eUNQ/zyx+o=
20250916093608_kurzarbeit.up.sql h1:pTiw0VfGaf26mhJg4wf98Fqwn1kShJ+PiN2PiM4q1kk=

19
package-lock.json generated
View File

@@ -10,7 +10,8 @@
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/material-symbols-light": "^1.2.33", "@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": { "node_modules/@antfu/install-pkg": {
@@ -1294,6 +1295,22 @@
"pathe": "^2.0.3" "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": { "node_modules/quansync": {
"version": "0.2.11", "version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",

View File

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