diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d765b0a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Ensure all text files use LF line endings +* text=auto eol=lf diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..62c456c --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,72 @@ +name: GoLang 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 + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: Backend/go.mod + - uses: https://gitea.com/actions/go-hashfiles@v0.0.1 + id: hash-go + with: + patterns: | + go.mod + go.sum + - name: cache go + id: cache-go + uses: actions/cache@v4 + with: + path: |- + /go_path + /go_cache + key: arbeitszeitmessung-${{ steps.hash-go.outputs.hash }} + restore-keys: |- + arbeitszeitmessung- + - name: Run Go Tests + run: cd Backend && go test ./... + build: + needs: testing + name: Build Go Image and Upload + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: git.letsstein.de + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: git.letsstein.de/tom/arbeitszeitmessung:latest + context: Backend diff --git a/.gitignore b/.gitignore index e7c242b..4e80b98 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ DB/pg_data .vscode node_modules +atlas.hcl +.scannerwork diff --git a/Backend/Makefile b/Backend/Makefile new file mode 100644 index 0000000..34598d0 --- /dev/null +++ b/Backend/Makefile @@ -0,0 +1,6 @@ +test: + mkdir -p .test + go test ./... -coverprofile=.test/coverage.out -json > .test/report.json + +scan: + sonar-scanner -Dsonar.token=sqa_ca8394c93a728d6cff96703955288d8902c15200 diff --git a/Backend/database.go b/Backend/database.go index f814974..bad1b77 100644 --- a/Backend/database.go +++ b/Backend/database.go @@ -5,9 +5,11 @@ import ( "arbeitszeitmessung/models" "database/sql" "fmt" + + _ "github.com/lib/pq" ) -func OpenDatabase() (*sql.DB, error) { +func OpenDatabase() (models.IDatabase, error) { dbHost := helper.GetEnv("POSTGRES_HOST", "localhost") dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung") dbUser := helper.GetEnv("POSTGRES_API_USER", "api_nutzer") @@ -16,30 +18,3 @@ func OpenDatabase() (*sql.DB, error) { connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=Europe/Berlin", dbUser, dbPassword, dbHost, dbName) return sql.Open("postgres", connStr) } - -func GetBookingsByCardID(db *sql.DB, card_id string) ([]models.Booking, error) { - qStr, err := db.Prepare((`SELECT * FROM anwesenheit WHERE card_id = $1`)) - if err != nil { - return nil, err - } - var bookings []models.Booking - rows, err := qStr.Query(card_id) - if err == sql.ErrNoRows { - return bookings, err - } - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - var booking models.Booking - if err := rows.Scan(&booking.CounterId, &booking.Timestamp, &booking.CardUID, &booking.GeraetID, &booking.CheckInOut); err != nil { - return bookings, err - } - bookings = append(bookings, booking) - } - if err = rows.Err(); err != nil { - return bookings, err - } - return bookings, nil -} diff --git a/Backend/endpoints/team.go b/Backend/endpoints/team.go index c28cc72..33c5569 100644 --- a/Backend/endpoints/team.go +++ b/Backend/endpoints/team.go @@ -17,26 +17,11 @@ func TeamHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: submitReport(w, r) - break case http.MethodGet: showWeeks(w, r) - break default: http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) - break } - // user, err := (*models.User).GetUserFromSession(nil, Session, r.Context()) - // if err != nil { - // log.Println("No user found with the given personal number!") - // http.Redirect(w, r, "/user/login", http.StatusSeeOther) - // return - // } - // var userWorkDays []models.WorkDay - // userWorkDays = (*models.WorkDay).GetWorkDays(nil, user.CardUID, time.Date(2025, time.February, 24, 0, 0, 0, 0, time.Local), time.Date(2025, time.February, 24+7, 0, 0, 0, 0, time.Local)) - // log.Println("User:", user) - // teamMembers, err := user.GetTeamMembers() - // getWeeksTillNow(time.Now().AddDate(0, 0, -14)) - // templates.TeamPage(teamMembers, userWorkDays).Render(r.Context(), w) } func submitReport(w http.ResponseWriter, r *http.Request) { @@ -48,8 +33,8 @@ func submitReport(w http.ResponseWriter, r *http.Request) { userPN, _ := strconv.Atoi(r.FormValue("user")) _weekTs := r.FormValue("week") weekTs, err := time.Parse(time.DateOnly, _weekTs) - user, err := (*models.User).GetByPersonalNummer(nil, userPN) - workWeek := (*models.WorkWeek).GetWeek(nil, user, weekTs, false) + user, err := models.GetUserByPersonalNr(userPN) + workWeek := models.NewWorkWeek(user, weekTs, true) if err != nil { log.Println("Could not get user!") @@ -58,11 +43,9 @@ func submitReport(w http.ResponseWriter, r *http.Request) { switch r.FormValue("method") { case "send": - err = workWeek.Send() - break + err = workWeek.SendWeek() case "accept": err = workWeek.Accept() - break default: break } @@ -80,14 +63,14 @@ func showWeeks(w http.ResponseWriter, r *http.Request) { return } submissionDate := r.URL.Query().Get("submission_date") - lastSub := user.GetLastSubmission() + lastSub := user.GetLastWorkWeekSubmission() if submissionDate != "" { submissionDate, err := time.Parse("2006-01-02", submissionDate) if err == nil { - lastSub = getMonday(submissionDate) + lastSub = helper.GetMonday(submissionDate) } } - userWeek := (*models.WorkWeek).GetWeek(nil, user, lastSub, true) + userWeek := models.NewWorkWeek(user, lastSub, true) var workWeeks []models.WorkWeek teamMembers, err := user.GetTeamMembers() @@ -98,38 +81,3 @@ func showWeeks(w http.ResponseWriter, r *http.Request) { // isRunningWeek := time.Since(lastSub) < 24*5*time.Hour //the last submission is this week and cannot be send yet templates.TeamPage(workWeeks, userWeek).Render(r.Context(), w) } - -func getWeeksTillNow(lastWeek time.Time) []time.Time { - var weeks []time.Time - if lastWeek.After(time.Now()) { - log.Println("Timestamp is after today, no weeks till now!") - return weeks - } - if lastWeek.Weekday() != time.Monday { - if lastWeek.Weekday() == time.Sunday { - lastWeek = lastWeek.AddDate(0, 0, -6) - } else { - lastWeek = lastWeek.AddDate(0, 0, -int(lastWeek.Weekday()-1)) - } - } - if time.Since(lastWeek) < 24*5*time.Hour { - log.Println("Timestamp in running week, cannot split!") - } - - for t := lastWeek; t.Before(time.Now()); t = t.Add(7 * 24 * time.Hour) { - weeks = append(weeks, t) - } - log.Println(weeks) - return weeks -} - -func getMonday(ts time.Time) time.Time { - if ts.Weekday() != time.Monday { - if ts.Weekday() == time.Sunday { - ts = ts.AddDate(0, 0, -6) - } else { - ts = ts.AddDate(0, 0, -int(ts.Weekday()-1)) - } - } - return ts -} diff --git a/Backend/endpoints/team_presence.go b/Backend/endpoints/team_presence.go index 5ca18e0..b313627 100644 --- a/Backend/endpoints/team_presence.go +++ b/Backend/endpoints/team_presence.go @@ -8,24 +8,21 @@ import ( "net/http" ) -func TeamPresenceHandler(w http.ResponseWriter, r *http.Request){ +func TeamPresenceHandler(w http.ResponseWriter, r *http.Request) { helper.RequiresLogin(Session, w, r) helper.SetCors(w) switch r.Method { case http.MethodGet: teamPresence(w, r) - break case http.MethodOptions: // just support options header for non GET Requests from SWAGGER w.WriteHeader(http.StatusOK) - break default: http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) - break } } -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()) if err != nil { log.Println("Error getting user!", err) @@ -37,8 +34,7 @@ func teamPresence(w http.ResponseWriter, r *http.Request){ teamPresence[present] = append(teamPresence[present], user) } - - if(err != nil){ + if err != nil { log.Println("Error getting team", err) } templates.TeamPresencePage(teamPresence).Render(r.Context(), w) diff --git a/Backend/endpoints/time-create.go b/Backend/endpoints/time-create.go index ddeb0a4..5f64103 100644 --- a/Backend/endpoints/time-create.go +++ b/Backend/endpoints/time-create.go @@ -16,24 +16,20 @@ func TimeCreateHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPut: createBooking(w, r) - break case http.MethodGet: createBooking(w, r) - break case http.MethodOptions: // just support options header for non GET Requests from SWAGGER w.WriteHeader(http.StatusOK) - break default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - break } } // Creates a booking from the http query params -> no body needed // after that entry wi'll be written to database and the booking is returned as json func createBooking(w http.ResponseWriter, r *http.Request) { - if !checkPassword(r) { + if !verifyToken(r) { log.Println("Wrong or no API key provided!") http.Error(w, "Wrong or no API key provided", http.StatusUnauthorized) return @@ -54,20 +50,20 @@ func createBooking(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(booking) + return } - w.WriteHeader(http.StatusBadRequest) + http.Error(w, "Cannot verify booking, maybe missing a parameter", http.StatusBadRequest) } -func checkPassword(r *http.Request) bool { +func verifyToken(r *http.Request) bool { authToken := helper.GetEnv("API_TOKEN", "dont_access") authHeaders := r.Header.Get("Authorization") - _authStart := len("Bearer ") - if len(authHeaders) <= _authStart { + if len(authHeaders) <= 7 { //len "Bearer " authHeaders = r.URL.Query().Get("api_key") - _authStart = 0 - if len(authHeaders) <= _authStart { + if len(authHeaders) <= 0 { return false } + return authToken == authHeaders } - return authToken == authHeaders[_authStart:] + return authToken == authHeaders[7:] } diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index 36abc91..c5edf3f 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -20,30 +20,26 @@ func TimeHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: getBookings(w, r) - break case http.MethodPost: updateBooking(w, r) - break case http.MethodOptions: // just support options header for non GET Requests from SWAGGER w.WriteHeader(http.StatusOK) - break default: http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) - break } } -func parseTimestamp(r *http.Request, get_key string, fallback string) (time.Time, error) { - _timestamp_get := r.URL.Query().Get(get_key) - if _timestamp_get == "" { - _timestamp_get = fallback +func parseTimestamp(r *http.Request, getKey string, fallback string) (time.Time, error) { + getTimestamp := r.URL.Query().Get(getKey) + if getTimestamp == "" { + getTimestamp = fallback } - timestamp_get, err := time.Parse("2006-01-02", _timestamp_get) + Timestamp, err := time.Parse("2006-01-02", getTimestamp) if err != nil { return time.Now(), err } - return timestamp_get, nil + return Timestamp, nil } // Returns bookings from DB with similar card uid -> checks for card uid in http query params @@ -75,6 +71,20 @@ func getBookings(w http.ResponseWriter, r *http.Request) { return workDays[i].Day.After(workDays[j].Day) }) + lastSub := user.GetLastWorkWeekSubmission() + var aggregatedOvertime time.Duration + for _, days := range workDays { + if days.Day.Before(lastSub) { + continue + } + aggregatedOvertime += days.CalcOvertime(user) + } + if reportedOvertime, err := user.GetReportedOvertime(); err == nil { + user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute) + } else { + log.Println("Cannot calculate overtime: ", err) + } + if r.Header.Get("Accept") == "application/json" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -82,8 +92,12 @@ func getBookings(w http.ResponseWriter, r *http.Request) { return } + if err != nil { + log.Println("Error calc overtime: ", err) + } + ctx := context.WithValue(r.Context(), "user", user) - templates.TimePage(workDays).Render(ctx, w) + templates.TimePage(workDays, lastSub).Render(ctx, w) } func updateBooking(w http.ResponseWriter, r *http.Request) { @@ -114,13 +128,12 @@ func updateBooking(w http.ResponseWriter, r *http.Request) { return } - newBooking := (*models.Booking).New(nil, user.CardUID, 0, int16(check_in_out)) + newBooking := (*models.Booking).New(nil, user.CardUID, 0, int16(check_in_out), 1) newBooking.Timestamp = timestamp - err = newBooking.InsertTimestamp() + err = newBooking.InsertWithTimestamp() if err != nil { - log.Println("Error inserting booking", err) + log.Printf("Error inserting booking %v -> %v\n", newBooking, err) } - break case "change": absenceType, err := strconv.Atoi(r.FormValue("absence")) if err != nil { @@ -151,9 +164,7 @@ func updateBooking(w http.ResponseWriter, r *http.Request) { booking.UpdateTime(parsedTime) } } - break } - getBookings(w, r) } @@ -163,7 +174,12 @@ func createAbsence(absenceType int, user models.User, loc *time.Location, r *htt log.Println("Cannot get date from input! Skipping absence creation", err) return } - absence := models.NewAbsence(user.CardUID, int8(absenceType), absenceDate) + + absence, err := models.NewAbsence(user.CardUID, absenceType, absenceDate) + if err != nil { + log.Println("Error creating absence!", err) + return + } err = absence.Insert() if err != nil { log.Println("Error inserting absence!", err) @@ -180,7 +196,7 @@ func getBookingsAPI(w http.ResponseWriter, r *http.Request) { return } - user, err := (*models.User).GetByPersonalNummer(nil, user_pn) + 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) diff --git a/Backend/endpoints/user-login.go b/Backend/endpoints/user-session.go similarity index 84% rename from Backend/endpoints/user-login.go rename to Backend/endpoints/user-session.go index ee19238..ffed8b9 100644 --- a/Backend/endpoints/user-login.go +++ b/Backend/endpoints/user-session.go @@ -21,20 +21,6 @@ func CreateSessionManager(lifetime time.Duration) *scs.SessionManager { return Session } -func LoginHandler(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - showLoginPage(w, r, false) - break - case http.MethodPost: - loginUser(w, r) - break - default: - http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) - break - } -} - func showLoginPage(w http.ResponseWriter, r *http.Request, failed bool) { r = r.WithContext(context.WithValue(r.Context(), "session", Session)) if helper.GetEnv("GO_ENV", "production") == "debug" { @@ -67,7 +53,7 @@ func loginUser(w http.ResponseWriter, r *http.Request) { return } - user, err := (*models.User).GetByPersonalNummer(nil, personal_nummer) + user, err := models.GetUserByPersonalNr(personal_nummer) if err != nil { log.Println("No user found under this personal number!") http.Error(w, "No user found!", http.StatusNotFound) @@ -83,5 +69,13 @@ func loginUser(w http.ResponseWriter, r *http.Request) { return } showLoginPage(w, r, false) - return +} + +func logoutUser(w http.ResponseWriter, r *http.Request) { + log.Println("Loggin out user!") + err := Session.Destroy(r.Context()) + if err != nil { + log.Println("Error destroying session!", err) + } + http.Redirect(w, r, "/user/login", http.StatusSeeOther) } diff --git a/Backend/endpoints/user-settings.go b/Backend/endpoints/user-settings.go index 7524f81..889267d 100644 --- a/Backend/endpoints/user-settings.go +++ b/Backend/endpoints/user-settings.go @@ -1,36 +1,12 @@ package endpoints import ( - "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "arbeitszeitmessung/templates" "log" "net/http" ) -func UserSettingsHandler(w http.ResponseWriter, r *http.Request) { - helper.RequiresLogin(Session, w, r) - switch r.Method { - case http.MethodGet: - showUserPage(w, r, 0) - break - case http.MethodPost: - switch r.FormValue("action") { - case "change-pass": - changePassword(w, r) - break - case "logout-user": - logoutUser(w, r) - break - } - - break - default: - http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) - break - } -} - // change user password and store salted hash in db func changePassword(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() @@ -45,7 +21,7 @@ func changePassword(w http.ResponseWriter, r *http.Request) { showUserPage(w, r, http.StatusBadRequest) return } - user, err := (*models.User).GetByPersonalNummer(nil, Session.GetInt(r.Context(), "user")) + user, err := models.GetUserByPersonalNr(Session.GetInt(r.Context(), "user")) if err != nil { log.Println("Error getting user!", err) showUserPage(w, r, http.StatusBadRequest) @@ -61,16 +37,6 @@ func changePassword(w http.ResponseWriter, r *http.Request) { showUserPage(w, r, http.StatusUnauthorized) } -func logoutUser(w http.ResponseWriter, r *http.Request) { - - err := Session.Destroy(r.Context()) - if err != nil { - log.Println("Error destroying session!", err) - } - http.Redirect(w, r, "/user/login", http.StatusSeeOther) -} - func showUserPage(w http.ResponseWriter, r *http.Request, status int) { templates.UserPage(status).Render(r.Context(), w) - return } diff --git a/Backend/endpoints/user.go b/Backend/endpoints/user.go new file mode 100644 index 0000000..540d117 --- /dev/null +++ b/Backend/endpoints/user.go @@ -0,0 +1,45 @@ +package endpoints + +import ( + "arbeitszeitmessung/helper" + "net/http" +) + +func UserHandler(w http.ResponseWriter, r *http.Request) { + switch r.PathValue("action") { + case "login": + LoginHandler(w, r) + case "settings": + UserSettingsHandler(w, r) + case "logout": + logoutUser(w, r) + } +} + +func LoginHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + showLoginPage(w, r, false) + case http.MethodPost: + loginUser(w, r) + default: + http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) + } +} + +func UserSettingsHandler(w http.ResponseWriter, r *http.Request) { + helper.RequiresLogin(Session, w, r) + switch r.Method { + case http.MethodGet: + showUserPage(w, r, 0) + case http.MethodPost: + switch r.FormValue("action") { + case "change-pass": + changePassword(w, r) + case "logout-user": + logoutUser(w, r) + } + default: + http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) + } +} diff --git a/Backend/go.mod b/Backend/go.mod index 4c04ddd..d5c6953 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -1,13 +1,20 @@ module arbeitszeitmessung -go 1.23 - -toolchain go1.23.6 +go 1.24.5 require github.com/lib/pq v1.10.9 -require github.com/a-h/templ v0.3.833 +require github.com/a-h/templ v0.3.943 require github.com/alexedwards/scs/v2 v2.8.0 -require github.com/joho/godotenv v1.5.1 +require ( + github.com/golang-migrate/migrate/v4 v4.18.3 + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + go.uber.org/atomic v1.7.0 // indirect +) diff --git a/Backend/go.sum b/Backend/go.sum index 8c124ff..82a9cc4 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -1,10 +1,74 @@ -github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU= -github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY= +github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM= +github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= +github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/Backend/helper/system_test.go b/Backend/helper/system_test.go new file mode 100644 index 0000000..dc944d9 --- /dev/null +++ b/Backend/helper/system_test.go @@ -0,0 +1,53 @@ +package helper + +import ( + "os" + "testing" + "time" +) + +func TestGetEnv(t *testing.T) { + os.Setenv("GO_TEST_VALUE", "123") + env := GetEnv("GO_TEST_VALUE", "") + if env != "123" { + t.Error("GetEnv() cannot find value") + } +} + +func TestGetEnvEmpty(t *testing.T) { + env := GetEnv("GO_TEST_NOVALUE", "123") + if env != "123" { + t.Errorf("GetEnv() did not use default value: want=%s got=%s", "123", env) + } +} + +func TestCacheCreate(t *testing.T) { + cacheFetch := func(key string) (any, error) { + return "123", nil + } + + cache := NewCache(1*time.Second, cacheFetch) + if cache.ttl != 1*time.Second { + t.Error("Error creating cache") + } +} + +func TestCacheFunction(t *testing.T) { + counter := 1 + cacheFetch := func(key string) (any, error) { + counter += 1 + return counter, nil + } + + cache := NewCache(1*time.Millisecond, cacheFetch) + valInit, err := cache.Get("TEST") + valCache, err := cache.Get("TEST") + time.Sleep(1 * time.Millisecond) + valNoCache, err := cache.Get("TEST") + if err != nil { + t.Errorf("Error getting key from Cache: %e", err) + } + if valInit != valCache || valCache != 2 || valNoCache != 3 { + t.Error("Caching does not resprect ttl.") + } +} diff --git a/Backend/helper/time.go b/Backend/helper/time.go new file mode 100644 index 0000000..70ace8a --- /dev/null +++ b/Backend/helper/time.go @@ -0,0 +1,37 @@ +package helper + +import ( + "fmt" + "time" +) + +func GetMonday(ts time.Time) time.Time { + if ts.Weekday() != time.Monday { + if ts.Weekday() == time.Sunday { + return ts.AddDate(0, 0, -6) + } else { + return ts.AddDate(0, 0, -int(ts.Weekday()-1)) + } + } + return ts +} + +// Converts duration to string +func FormatDuration(d time.Duration) string { + hours := int(d.Abs().Hours()) + minutes := int(d.Abs().Minutes()) % 60 + sign := "" + if d < 0 { + sign = "-" + } + switch { + case hours > 0 && minutes == 0: + return fmt.Sprintf("%s%dh", sign, hours) + case hours > 0: + return fmt.Sprintf("%s%dh %dmin", sign, hours, minutes) + case minutes > 0: + return fmt.Sprintf("%s%dmin", sign, minutes) + default: + return "" + } +} diff --git a/Backend/helper/time_test.go b/Backend/helper/time_test.go new file mode 100644 index 0000000..454d4e3 --- /dev/null +++ b/Backend/helper/time_test.go @@ -0,0 +1,37 @@ +package helper + +import ( + "testing" + "time" +) + +func TestGetMonday(t *testing.T) { + isMonday, err := time.Parse("2006-01-02", "2025-07-14") + notMonday, err := time.Parse("2006-01-02", "2025-07-16") + if err != nil || isMonday.Equal(notMonday) { + t.Errorf("U stupid? %e", err) + } + if GetMonday(isMonday) != isMonday || GetMonday(notMonday) != isMonday { + t.Error("Wrong date conversion!") + } +} + +func TestFormatDuration(t *testing.T) { + durations := []struct { + name string + duration time.Duration + }{ + {"2h", time.Duration(120 * time.Minute)}, + {"30min", time.Duration(30 * time.Minute)}, + {"1h 30min", time.Duration(90 * time.Minute)}, + {"-1h 30min", time.Duration(-90 * time.Minute)}, + {"", 0}, + } + for _, d := range durations { + t.Run(d.name, func(t *testing.T) { + if FormatDuration(d.duration) != d.name { + t.Error("Format missmatch in Formatduration.") + } + }) + } +} diff --git a/Backend/main.go b/Backend/main.go index 969ab60..272eb6f 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -22,22 +22,19 @@ func main() { if err != nil { log.Println("No .env file found in directory!") } - if(helper.GetEnv("GO_ENV", "production") == "debug") { + if helper.GetEnv("GO_ENV", "production") == "debug" { log.Println("Debug mode enabled") log.Println("Environment Variables") envs := os.Environ() - for _, e := range envs { - fmt.Println(e) - } + for _, e := range envs { + fmt.Println(e) + } } - - models.DB, err = OpenDatabase() if err != nil { log.Fatal(err) } - defer models.DB.Close() fs := http.FileServer(http.Dir("./static")) endpoints.CreateSessionManager(24 * time.Hour) @@ -48,8 +45,9 @@ func main() { server.HandleFunc("/time/new", endpoints.TimeCreateHandler) server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler)) server.HandleFunc("/logout", endpoints.LogoutHandler) - server.HandleFunc("/user/login", endpoints.LoginHandler) - server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) + server.HandleFunc("/user/{action}", endpoints.UserHandler) + // server.HandleFunc("/user/login", endpoints.LoginHandler) + // server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) server.HandleFunc("/team", endpoints.TeamHandler) server.HandleFunc("/team/presence", endpoints.TeamPresenceHandler) server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect)) diff --git a/Backend/models/absence.go b/Backend/models/absence.go index 2f352c6..c5184d0 100644 --- a/Backend/models/absence.go +++ b/Backend/models/absence.go @@ -1,52 +1,42 @@ package models import ( + "errors" "log" "time" ) type AbsenceType struct { - Value int8 - Label string -} - -const ( - AbsenceNone int8 = iota - AbsenceUrlaub - AbsenceKurzarbeit - AbsenceKrank - AbsenceKindkrank -) - -var AbsenceTypes = []AbsenceType{ - // {Value: AbsenceNone, Label: "Abwesenheit"}, - {Value: AbsenceUrlaub, Label: "Urlaub"}, - {Value: AbsenceKurzarbeit, Label: "Kurzarbeit"}, - {Value: AbsenceKrank, Label: "Krank"}, - {Value: AbsenceKindkrank, Label: "Kindkrank"}, -} - -var AbsenceTypesLabel = map[int8]string{ - 0: "None", - AbsenceUrlaub: "Urlaub", - AbsenceKurzarbeit: "Kurzarbeit", - AbsenceKrank: "Krank", - AbsenceKindkrank: "Kindkrank", + Id int8 + Name string + WorkTime float32 } type Absence struct { CounterId int CardUID string - AbwesenheitTyp int8 + AbwesenheitTyp AbsenceType Datum time.Time + // Comment string } -func NewAbsence(card_uid string, abwesenheit_typ int8, datum time.Time) Absence { +func NewAbsence(card_uid string, abwesenheit_typ int, datum time.Time) (Absence, error) { + if abwesenheit_typ < 0 { + return Absence{ + CardUID: card_uid, + AbwesenheitTyp: AbsenceType{0, "Custom absence", 100}, + Datum: datum, + }, nil + } + _absenceType, ok := GetAbsenceTypesCached()[int8(abwesenheit_typ)] + if !ok { + return Absence{}, errors.New("Invalid absencetype") + } return Absence{ CardUID: card_uid, - AbwesenheitTyp: abwesenheit_typ, + AbwesenheitTyp: _absenceType, Datum: datum, - } + }, nil } func (a *Absence) Insert() error { @@ -55,7 +45,8 @@ func (a *Absence) Insert() error { log.Println("Error preparing sql Statement", err) return err } - err = qStr.QueryRow(a.CardUID, a.AbwesenheitTyp, a.Datum).Scan(&a.CounterId) + defer qStr.Close() + err = qStr.QueryRow(a.CardUID, a.AbwesenheitTyp.Id, a.Datum).Scan(&a.CounterId) if err != nil { log.Println("Error executing insert statement", err) return err @@ -63,6 +54,92 @@ func (a *Absence) Insert() error { return nil } -func (a *Absence) GetStringType() string { - return AbsenceTypesLabel[a.AbwesenheitTyp] +func GetAbsenceById(counterId int) (Absence, error) { + var absence Absence = Absence{CounterId: counterId} + qStr, err := DB.Prepare("SELECT card_uid, abwesenheit_typ, datum FROM abwesenheit WHERE counter_id = $1;") + if err != nil { + return absence, err + } + defer qStr.Close() + err = qStr.QueryRow(counterId).Scan(&absence.CardUID, &absence.AbwesenheitTyp.Id, &absence.Datum) + if err != nil { + return absence, err + } + return absence, nil +} + +func GetAbsencesByCardUID(card_uid string, tsFrom time.Time, tsTo time.Time) ([]Absence, error) { + var absences []Absence + qStr, err := DB.Prepare("SELECT counter_id, abwesenheit_typ, datum FROM abwesenheit WHERE card_uid = $1 AND datum BETWEEN $2::DATE AND $3::DATE ORDER BY datum;") + if err != nil { + return absences, err + } + defer qStr.Close() + rows, err := qStr.Query(card_uid, tsFrom, tsTo) + if err != nil { + return absences, err + } + defer rows.Close() + for rows.Next() { + var absence Absence + if err := rows.Scan(&absence.CounterId, &absence.AbwesenheitTyp.Id, &absence.Datum); err != nil { + return absences, err + } + absence.AbwesenheitTyp, err = GetAbsenceTypeById(absence.AbwesenheitTyp.Id) + if err == nil { + absences = append(absences, absence) + } else { + log.Println("Cannot populate absence type!", err) + } + } + if err = rows.Err(); err != nil { + return absences, err + } + return absences, nil +} + +func GetAbsenceTypes() (map[int8]AbsenceType, error) { + var types = make(map[int8]AbsenceType) + qStr, err := DB.Prepare("SELECT abwesenheit_id, abwesenheit_name, arbeitszeit_equivalent FROM s_abwesenheit_typen;") + if err != nil { + return types, err + } + defer qStr.Close() + rows, err := qStr.Query() + if err != nil { + log.Println("Error getting abwesenheit rows!", err) + return types, err + } + defer rows.Close() + for rows.Next() { + var absenceType AbsenceType + if err := rows.Scan(&absenceType.Id, &absenceType.Name, &absenceType.WorkTime); err != nil { + log.Println("Error scanning absence row!", err) + } + types[absenceType.Id] = absenceType + } + return types, nil +} + +func GetAbsenceTypesCached() map[int8]AbsenceType { + types, err := definedTypes.Get("s_abwesenheit_typen") + if err != nil { + return map[int8]AbsenceType{} + } + return types.(map[int8]AbsenceType) +} + +func GetAbsenceTypeById(absenceTypeId int8) (AbsenceType, error) { + var absenceType AbsenceType = AbsenceType{Id: absenceTypeId} + + qStr, err := DB.Prepare("SELECT abwesenheit_name, arbeitszeit_equivalent FROM s_abwesenheit_typen WHERE abwesenheit_id = $1;") + if err != nil { + return absenceType, err + } + defer qStr.Close() + err = qStr.QueryRow(absenceTypeId).Scan(&absenceType.Name, &absenceType.WorkTime) + if err != nil { + return absenceType, err + } + return absenceType, nil } diff --git a/Backend/models/booking.go b/Backend/models/booking.go index 47c2c47..a293ee5 100644 --- a/Backend/models/booking.go +++ b/Backend/models/booking.go @@ -13,45 +13,77 @@ import ( type SameBookingError struct{} +type BookingType struct { + Id int8 + Name string +} + func (e SameBookingError) Error() string { return "the same booking already exists!" } type Booking struct { - CardUID string `json:"card_uid"` - GeraetID int16 `json:"geraet_id"` - CheckInOut int16 `json:"check_in_out"` - Timestamp time.Time `json:"timestamp"` - CounterId int `json:"counter_id"` + CardUID string `json:"card_uid"` + GeraetID int16 `json:"geraet_id"` + CheckInOut int16 `json:"check_in_out"` + Timestamp time.Time `json:"timestamp"` + CounterId int `json:"counter_id"` + BookingType BookingType `json:"booking_type"` } -var DB *sql.DB +type IDatabase interface { + Prepare(query string) (*sql.Stmt, error) + Exec(query string, args ...any) (sql.Result, error) +} -func (b *Booking) New(card_uid string, geraet_id int16, check_in_out int16) Booking { +var DB IDatabase + +func (b *Booking) New(cardUid string, gereatId int16, checkInOut int16, typeId int8) Booking { + bookingType, err := GetBookingTypeById(typeId) + if err != nil { + log.Printf("Cannot get booking type %d, from database!", typeId) + } return Booking{ - CardUID: card_uid, - GeraetID: geraet_id, - CheckInOut: check_in_out, + CardUID: cardUid, + GeraetID: gereatId, + CheckInOut: checkInOut, + BookingType: bookingType, } } func (b *Booking) FromUrlParams(params url.Values) Booking { var booking Booking + if _check_in_out, err := strconv.Atoi(params.Get("check_in_out")); err == nil { booking.CheckInOut = int16(_check_in_out) } if _geraet_id, err := strconv.Atoi(params.Get("geraet_id")); err == nil { booking.GeraetID = int16(_geraet_id) } + if _booking_type, err := strconv.Atoi(params.Get("booking_type")); err == nil { + booking.BookingType.Id = int8(_booking_type) + } booking.CardUID = params.Get("card_uid") + return booking } func (b *Booking) Verify() bool { //check for overlapping time + arbeitszeit verstoß if b.CardUID == "" { //|| b.GeraetID == 0 || b.CheckInOut == 0 { + log.Println("Booking verify failed invalid CardUID!") return false } + if b.CheckInOut == 0 { + log.Println("Booking verify failed invalid CheckInOut!") + return false + } + if bookingType, err := GetBookingTypeById(b.BookingType.Id); err != nil { + log.Println("Booking verify failed invalid BookingType.Id!") + return false + } else { + b.BookingType.Name = bookingType.Name + } return true } @@ -59,26 +91,26 @@ func (b *Booking) Insert() error { if !checkLastBooking(*b) { return SameBookingError{} } - stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out) VALUES ($1, $2, $3) RETURNING counter_id, timestamp`)) + stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, anwesenheit_typ) VALUES ($1, $2, $3, $4) RETURNING counter_id, timestamp`)) if err != nil { return err } - err = stmt.QueryRow(b.CardUID, b.GeraetID, b.CheckInOut).Scan(&b.CounterId, &b.Timestamp) + err = stmt.QueryRow(b.CardUID, b.GeraetID, b.CheckInOut, b.BookingType.Id).Scan(&b.CounterId, &b.Timestamp) if err != nil { return err } return nil } -func (b *Booking) InsertTimestamp() error { +func (b *Booking) InsertWithTimestamp() error { if b.Timestamp.IsZero() { return b.Insert() } - stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, timestamp) VALUES ($1, $2, $3, $4) RETURNING counter_id`)) + stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, anwesenheit_typ, timestamp) VALUES ($1, $2, $3, $4, $5) RETURNING counter_id`)) if err != nil { return err } - err = stmt.QueryRow(b.CardUID, b.GeraetID, b.CheckInOut, b.Timestamp).Scan(&b.CounterId) + err = stmt.QueryRow(b.CardUID, b.GeraetID, b.CheckInOut, b.BookingType.Id, b.Timestamp).Scan(&b.CounterId) if err != nil { return err } @@ -87,18 +119,15 @@ func (b *Booking) InsertTimestamp() error { func (b *Booking) GetBookingById(booking_id int) (Booking, error) { var booking Booking - qStr, err := DB.Prepare((`SELECT counter_id, timestamp, card_uid, geraet_id, check_in_out FROM anwesenheit WHERE counter_id = $1`)) + qStr, err := DB.Prepare((`SELECT counter_id, timestamp, card_uid, geraet_id, check_in_out, anwesenheit_typ FROM anwesenheit WHERE counter_id = $1`)) if err != nil { return booking, err } - err = qStr.QueryRow(booking_id).Scan(&booking.CounterId, &booking.Timestamp, &booking.CardUID, &booking.GeraetID, &booking.CheckInOut) + // TODO: also get booking type name + err = qStr.QueryRow(booking_id).Scan(&booking.CounterId, &booking.Timestamp, &booking.CardUID, &booking.GeraetID, &booking.CheckInOut, &booking.BookingType.Id) if err != nil { return booking, err } - // if !booking.Verify() { - // fmt.Printf("Booking verification failed! %d", ) - // return booking, nil - // } return booking, nil } @@ -254,6 +283,7 @@ func (b *Booking) UpdateTime(newTime time.Time) { } log.Println("Updating") b.Update(newBooking) + // TODO Check verify b.Verify() b.Save() } @@ -261,3 +291,49 @@ func (b *Booking) UpdateTime(newTime time.Time) { func (b *Booking) ToString() string { return fmt.Sprintf("Booking %d: at: %s, as type: %d", b.CounterId, b.Timestamp.Format("15:04"), b.CheckInOut) } + +func GetBookingTypes() ([]BookingType, error) { + var types []BookingType + qStr, err := DB.Prepare("SELECT anwesenheit_id, anwesenheit_name FROM s_anwesenheit_typen;") + if err != nil { + return types, err + } + defer qStr.Close() + rows, err := qStr.Query() + if err != nil { + log.Println("Error getting anwesenheit rows!", err) + return types, err + } + defer rows.Close() + for rows.Next() { + var bookingType BookingType + if err := rows.Scan(&bookingType.Id, &bookingType.Name); err != nil { + log.Println("Error scanning row!", err) + } + types = append(types, bookingType) + } + return types, nil +} + +func GetBookingTypeById(bookingTypeId int8) (BookingType, error) { + var bookingType BookingType = BookingType{Id: bookingTypeId} + + qStr, err := DB.Prepare("SELECT anwesenheit_name FROM s_anwesenheit_typen WHERE anwesenheit_id = $1;") + if err != nil { + return bookingType, err + } + defer qStr.Close() + err = qStr.QueryRow(bookingTypeId).Scan(&bookingType.Name) + if err != nil { + return bookingType, err + } + return bookingType, nil +} + +func GetBookingTypesCached() []BookingType { + types, err := definedTypes.Get("s_anwesenheit_typen") + if err != nil { + return []BookingType{} + } + return types.([]BookingType) +} diff --git a/Backend/models/database.go b/Backend/models/database.go new file mode 100644 index 0000000..c114c4a --- /dev/null +++ b/Backend/models/database.go @@ -0,0 +1,15 @@ +package models + +import ( + "arbeitszeitmessung/helper" +) + +var definedTypes = helper.NewCache(3600, func(key string) (any, error) { + switch key { + case "s_abwesenheit_typen": + return GetAbsenceTypes() + case "s_anwesenheit_typen": + return GetBookingTypes() + } + return nil, nil +}) diff --git a/Backend/models/db_test.go b/Backend/models/db_test.go new file mode 100644 index 0000000..629bd2d --- /dev/null +++ b/Backend/models/db_test.go @@ -0,0 +1,69 @@ +package models_test + +import ( + "arbeitszeitmessung/helper" + "arbeitszeitmessung/models" + "database/sql" + "fmt" + "log" + "testing" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" + _ "github.com/lib/pq" +) + +type DBFixture struct { + Database models.IDatabase + TX *sql.Tx +} + +func SetupDBFixture(t *testing.T) *DBFixture { + t.Helper() + + dbHost := helper.GetEnv("POSTGRES_HOST", "localhost") + dbPort := helper.GetEnv("POSTGRES_PORT", "5433") + dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung") + dbUser := helper.GetEnv("POSTGRES_USER", "postgres") + dbPassword := helper.GetEnv("POSTGRES_PASSWORD", "password") + + connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable&TimeZone=Europe/Berlin", dbUser, dbPassword, dbHost, dbPort, dbName) + + db, err := sql.Open("postgres", connStr) + if err != nil { + t.Fatalf("failed to connect to 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() + if err != nil { + t.Fatalf("Failed to start transaction: %v", err) + } + + t.Cleanup(func() { + tx.Rollback() + db.Close() + }) + + return &DBFixture{ + Database: tx, + TX: tx, + } +} + +func MigrateDB(db *sql.DB, fileUrl string) error { + driver, err := postgres.WithInstance(db, &postgres.Config{}) + if err != nil { + log.Fatalln("Error starting migration", err) + } + m, err := migrate.NewWithDatabaseInstance( + fileUrl, + "postgres", driver) + return m.Up() +} diff --git a/Backend/models/user.go b/Backend/models/user.go index 7c45242..56169d4 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -13,24 +13,26 @@ import ( ) type User struct { - CardUID string `json:"card_uid"` - Name string `json:"name"` - Vorname string `json:"vorname"` - PersonalNummer int `json:"personal_nummer"` - ArbeitszeitPerTag float32 `json:"arbeitszeit"` + CardUID string `json:"card_uid"` + Name string `json:"name"` + Vorname string `json:"vorname"` + PersonalNummer int `json:"personal_nummer"` + ArbeitszeitPerTag float32 `json:"arbeitszeit_per_tag"` + ArbeitszeitPerWoche float32 `json:"arbeitszeit_per_woche"` + Overtime time.Duration } func (u *User) GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) { var user User var err error if helper.GetEnv("GO_ENV", "production") == "debug" { - user, err = (*User).GetByPersonalNummer(nil, 123) + user, err = GetUserByPersonalNr(123) } else { if !Session.Exists(ctx, "user") { log.Println("No user in session storage!") return user, errors.New("No user in session storage!") } - user, err = (*User).GetByPersonalNummer(nil, Session.GetInt(ctx, "user")) + user, err = GetUserByPersonalNr(Session.GetInt(ctx, "user")) } if err != nil { log.Println("Cannot get user from session!") @@ -39,8 +41,27 @@ func (u *User) GetUserFromSession(Session *scs.SessionManager, ctx context.Conte return user, nil } +// Returns the actual overtime for this moment +func (u *User) GetReportedOvertime() (time.Duration, error) { + 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;") + if err != nil { + return 0, err + } + defer qStr.Close() + err = qStr.QueryRow(u.PersonalNummer).Scan(&overtimeReport) + if err != nil { + return 0, err + } + overtime = time.Duration(overtimeReport * float64(time.Hour)).Round(time.Minute) + log.Println("Overtime from wochen_report: ", overtime) + return overtime, nil +} + func (u *User) GetAll() ([]User, error) { - qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname FROM personal_daten;`)) + qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname FROM s_personal_daten;`)) var users []User if err != nil { fmt.Printf("Error preparing query statement %v\n", err) @@ -86,23 +107,23 @@ func (u *User) CheckAnwesenheit() bool { // Creates a new booking for the user -> check_in_out will be 254 for automatic check out func (u *User) CheckOut() error { - booking := (*Booking).New(nil, u.CardUID, 0, 254) + booking := (*Booking).New(nil, u.CardUID, 0, 254, 1) err := booking.Insert() if err != nil { - fmt.Printf("Error inserting booking %v\n", err) + fmt.Printf("Error inserting booking %v -> %v\n", booking, err) return err } return nil } -func (u *User) GetByPersonalNummer(personalNummer int) (User, error) { +func GetUserByPersonalNr(personalNummer int) (User, error) { var user User - qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag FROM personal_daten WHERE personal_nummer = $1;`)) + qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten WHERE personal_nummer = $1;`)) if err != nil { return user, err } - err = qStr.QueryRow(personalNummer).Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag) + err = qStr.QueryRow(personalNummer).Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche) if err != nil { return user, err @@ -146,7 +167,7 @@ func (u *User) ChangePass(password, newPassword string) (bool, error) { func (u *User) GetTeamMembers() ([]User, error) { var teamMembers []User - qStr, err := DB.Prepare(`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag FROM personal_daten WHERE vorgesetzter_pers_nr = $1`) + qStr, err := DB.Prepare(`SELECT personal_nummer FROM s_personal_daten WHERE vorgesetzter_pers_nr = $1`) if err != nil { return teamMembers, err } @@ -158,9 +179,11 @@ func (u *User) GetTeamMembers() ([]User, error) { } defer rows.Close() for rows.Next() { - user, err := parseUser(rows) + var personalNr int + err := rows.Scan(&personalNr) + user, err := GetUserByPersonalNr(personalNr) if err != nil { - log.Println("Error parsing user!") + log.Println("Error getting user!") return teamMembers, err } teamMembers = append(teamMembers, user) @@ -205,8 +228,8 @@ func parseUser(rows *sql.Rows) (User, error) { return user, nil } -// returns the start of the week, the last submission was made, submission == first booking or last send booking_report to team leader -func (u *User) GetLastSubmission() time.Time { +// returns the start of the week, the last submission was made, submission == first booking or last send wochen_report to team leader +func (u *User) GetLastWorkWeekSubmission() time.Time { var lastSub time.Time qStr, err := DB.Prepare(` SELECT COALESCE( @@ -223,10 +246,8 @@ func (u *User) GetLastSubmission() time.Time { log.Println("Error executing query!", err) return lastSub } - log.Println("From DB: ", lastSub) lastSub = getMonday(lastSub) lastSub = lastSub.Round(24 * time.Hour) - log.Println("After truncate: ", lastSub) return lastSub } @@ -234,7 +255,7 @@ func (u *User) GetFromCardUID(card_uid string) (User, error) { user := User{} var err error - qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag FROM personal_daten WHERE card_uid = $1;`)) + qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag FROM s_personal_daten WHERE card_uid = $1;`)) if err != nil { return user, err } diff --git a/Backend/models/user_test.go b/Backend/models/user_test.go new file mode 100644 index 0000000..323f925 --- /dev/null +++ b/Backend/models/user_test.go @@ -0,0 +1,56 @@ +package models_test + +import ( + "arbeitszeitmessung/models" + "database/sql" + "testing" +) + +var testUser models.User = models.User{Vorname: "Kim", Name: "Mustermensch", PersonalNummer: 456, CardUID: "aaaa-aaaa", ArbeitszeitPerTag: 8} + +func SetupUserFixture(t *testing.T, db models.IDatabase) { + 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 +(456, 't', 'Kim', 'Mustermensch', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'aaaa-aaaa', 1, 8, '07:00:00', '20:00:00', 0);`) +} + +func TestGetUserByPersonalNr(t *testing.T) { + tc := SetupDBFixture(t) + SetupUserFixture(t, tc.Database) + + models.DB = tc.Database + + user, err := models.GetUserByPersonalNr(testUser.PersonalNummer) + if err != nil { + t.Fatal(err) + } + if user != testUser { + t.Error("Retrieved user not the same as testUser!") + } + + _, err = models.GetUserByPersonalNr(000) + if err != sql.ErrNoRows { + t.Error("Wrong error handling, when retrieving wrong personalnummer") + } +} + +func TestCheckAnwesenheit(t *testing.T) { + tc := SetupDBFixture(t) + models.DB = tc.Database + SetupUserFixture(t, tc.Database) + + var actual bool + + if actual = testUser.CheckAnwesenheit(); actual != false { + 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);") + if actual = testUser.CheckAnwesenheit(); actual != true { + 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);") + if actual = testUser.CheckAnwesenheit(); actual != false { + t.Errorf("Checkabwesenheit with 'gehen' booking should be false but is %t", actual) + } +} diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index d6d637a..734e935 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -4,7 +4,6 @@ import ( "arbeitszeitmessung/helper" "database/sql" "encoding/json" - "fmt" "log" "strconv" "time" @@ -82,7 +81,7 @@ func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay LEFT JOIN ordered_bookings b ON d.work_date = b.work_date LEFT JOIN abwesenheiten a ON d.work_date = a.work_date GROUP BY d.work_date, a.abwesenheit_typ - ORDER BY d.work_date;`) + ORDER BY d.work_date ASC;`) if err != nil { log.Println("Error preparing SQL statement", err) @@ -118,8 +117,8 @@ func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay } if absenceType.Valid { - workDay.Absence = NewAbsence(card_uid, int8(absenceType.Int16), workDay.Day) - log.Println("Found absence", workDay.Absence) + 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)) { @@ -127,10 +126,12 @@ func (d *WorkDay) GetWorkDays(card_uid string, tsFrom, tsTo time.Time) []WorkDay } else { workDay.calcPauseTime() } - if emptyDays || len(workDay.Bookings) > 0 || (workDay.Absence != Absence{}) { + if emptyDays && workDay.Day.Weekday() >= 1 && workDay.Day.Weekday() <= 5 { workDays = append(workDays, workDay) - } else { - log.Println("no booking on day", workDay.Day.Format("02.01.2006")) + } 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 { @@ -180,23 +181,9 @@ func (d *WorkDay) getWorkTime() { d.calcPauseTime() } -// Converts duration to string -func formatDuration(d time.Duration) string { - hours := int(d.Abs().Hours()) - minutes := int(d.Abs().Minutes()) % 60 - switch { - case hours > 0: - return fmt.Sprintf("%dh %dmin", hours, minutes) - case minutes > 0: - return fmt.Sprintf("%dmin", minutes) - default: - return "" - } -} - func (d *WorkDay) GetWorkTimeString() (string, string) { - workString := formatDuration(d.workTime) - pauseString := formatDuration(d.pauseTime) + workString := helper.FormatDuration(d.workTime) + pauseString := helper.FormatDuration(d.pauseTime) return workString, pauseString } @@ -210,7 +197,17 @@ func (d *WorkDay) RequiresAction() bool { // 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)) + 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 +} diff --git a/Backend/models/workWeek.go b/Backend/models/workWeek.go index 1ce0a30..c093c58 100644 --- a/Backend/models/workWeek.go +++ b/Backend/models/workWeek.go @@ -1,18 +1,26 @@ package models import ( + "arbeitszeitmessung/helper" "database/sql" "errors" "log" "time" ) +// Workweeks are + type WorkWeek struct { - Id int - WorkDays []WorkDay - User User - WeekStart time.Time - WorkHours time.Duration + Id int + WorkDays []WorkDay + Absences []Absence + User User + WeekStart time.Time + Worktime time.Duration + Overtime time.Duration + Status WeekStatus + overtimeDiff time.Duration + worktimeDiff time.Duration } type WeekStatus int8 @@ -21,59 +29,92 @@ const ( WeekStatusNone WeekStatus = iota WeekStatusSent WeekStatusAccepted + WeekStatusDifferences ) -func (w *WorkWeek) GetWeek(user User, tsMonday time.Time, populateDays bool) WorkWeek { - var week WorkWeek - if populateDays { - week.WorkDays = (*WorkDay).GetWorkDays(nil, user.CardUID, tsMonday, tsMonday.Add(7*24*time.Hour)) - week.WorkHours = aggregateWorkTime(week.WorkDays) +func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek { + var week WorkWeek = WorkWeek{ + User: user, + WeekStart: tsMonday, + Status: WeekStatusNone, + } + if populate { + week.PopulateWithBookings(0, 0) } - week.User = user - week.WeekStart = tsMonday return week } +func (w *WorkWeek) PopulateWithBookings(overtime time.Duration, worktime time.Duration) { + w.WorkDays = (*WorkDay).GetWorkDays(nil, w.User.CardUID, w.WeekStart, w.WeekStart.Add(7*24*time.Hour)) + if absences, err := GetAbsencesByCardUID(w.User.CardUID, w.WeekStart, w.WeekStart.Add(7*24*time.Hour)); err == nil { + w.Absences = absences + } else { + log.Printf("Error populating absences in workWeek (%s): %v", w.WeekStart, err) + } + w.Worktime = w.aggregateWorkTime() + w.Overtime = w.Worktime - time.Duration(w.User.ArbeitszeitPerWoche*float32(time.Hour)).Round(time.Minute) + + if overtime == 0 && worktime == 0 { + return + } + + if overtime != w.Overtime || worktime != w.Worktime { + w.Status = WeekStatusDifferences + w.overtimeDiff = overtime + w.worktimeDiff = worktime + } +} + func (w *WorkWeek) CheckStatus() WeekStatus { - weekStatus := WeekStatusNone + if w.Status != WeekStatusNone { + return w.Status + } + if DB == nil { + log.Println("Cannot access Database!") + return w.Status + } qStr, err := DB.Prepare(`SELECT bestaetigt FROM wochen_report WHERE woche_start = $1::DATE AND personal_nummer = $2;`) if err != nil { log.Println("Error preparing SQL statement", err) - return weekStatus + return w.Status } defer qStr.Close() var beastatigt bool err = qStr.QueryRow(w.WeekStart, w.User.PersonalNummer).Scan(&beastatigt) if err == sql.ErrNoRows { - return weekStatus + return w.Status } if err != nil { log.Println("Error querying database", err) - return weekStatus + return w.Status } if beastatigt { - weekStatus = WeekStatusAccepted + w.Status = WeekStatusAccepted } else { - weekStatus = WeekStatusSent + w.Status = WeekStatusSent } - return weekStatus + return w.Status } func (w *WorkWeek) GetWorkHourString() string { - return formatDuration(w.WorkHours) + return helper.FormatDuration(w.Worktime) } -func aggregateWorkTime(days []WorkDay) time.Duration { +func (w *WorkWeek) aggregateWorkTime() time.Duration { var workTime time.Duration - for _, day := range days { + for _, day := range w.WorkDays { workTime += day.workTime } + for _, absences := range w.Absences { + absenceWorkTime := absences.AbwesenheitTyp.WorkTime - (absences.AbwesenheitTyp.WorkTime - w.User.ArbeitszeitPerTag) // workTime Equivalent of Absence is capped at user Worktime per Day + workTime += time.Duration(absenceWorkTime * float32(time.Hour)).Round(time.Minute) + } return workTime } func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek { var weeks []WorkWeek - qStr, err := DB.Prepare(`SELECT id, woche_start::DATE FROM wochen_report WHERE bestaetigt = FALSE AND personal_nummer = $1;`) + qStr, err := DB.Prepare(`SELECT id, woche_start::DATE, arbeitszeit, ueberstunden FROM wochen_report WHERE bestaetigt = FALSE AND personal_nummer = $1;`) if err != nil { log.Println("Error preparing SQL statement", err) return weeks @@ -87,14 +128,16 @@ func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek { } defer rows.Close() for rows.Next() { - var week WorkWeek - week.User = user - if err := rows.Scan(&week.Id, &week.WeekStart); err != nil { + var week WorkWeek = WorkWeek{User: user} + var workHours, overtime sql.NullFloat64 + if err := rows.Scan(&week.Id, &week.WeekStart, &workHours, &overtime); err != nil { log.Println("Error scanning row!", err) return weeks } - week.WorkDays = (*WorkDay).GetWorkDays(nil, user.CardUID, week.WeekStart, week.WeekStart.Add(7*24*time.Hour)) - week.WorkHours = aggregateWorkTime(week.WorkDays) + + if workHours.Valid && overtime.Valid { + 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) } if err = rows.Err(); err != nil { @@ -107,33 +150,34 @@ func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek { var ErrRunningWeek = errors.New("Week is in running week") // creates a new entry in the woche_report table with the given workweek -func (w *WorkWeek) Send() error { +func (w *WorkWeek) SendWeek() error { var qStr *sql.Stmt var err error + if time.Since(w.WeekStart) < 5*24*time.Hour { log.Println("Cannot send week, because it's the running week!") return ErrRunningWeek } + if w.CheckStatus() != WeekStatusNone { - qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE WHERE personal_nummer = $1 AND woche_start = $2;`) + qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE, arbeitszeit = $3, ueberstunden = $4 WHERE personal_nummer = $1 AND woche_start = $2;`) if err != nil { log.Println("Error preparing SQL statement", err) return err } } else { - qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start) VALUES ($1, $2);`) + qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden) VALUES ($1, $2, $3, $4);`) if err != nil { log.Println("Error preparing SQL statement", err) return err } } - _, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart) + _, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, w.Worktime.Hours(), w.Overtime.Hours()) if err != nil { log.Println("Error executing query!", err) return err } return nil - } func (w *WorkWeek) Accept() error { diff --git a/Backend/models/workWeek_test.go b/Backend/models/workWeek_test.go new file mode 100644 index 0000000..b94dd04 --- /dev/null +++ b/Backend/models/workWeek_test.go @@ -0,0 +1,50 @@ +package models_test + +import ( + "arbeitszeitmessung/models" + "testing" + "time" +) + +func SetupWorkWeekFixture(t *testing.T) models.WorkWeek { + t.Helper() + monday, err := time.Parse("2006-01-02", "2025-01-10") + if err != nil { + t.Fatal(err) + } + return models.WorkWeek{User: testUser, WeekStart: monday, Status: models.WeekStatusSent} +} + +func TestNewWorkWeekNoPopulate(t *testing.T) { + monday, err := time.Parse("2006-01-02", "2025-01-10") + if err != nil { + t.Fatal(err) + } + workWeek := models.NewWorkWeek(testUser, monday, false) + + if workWeek.User != testUser || workWeek.WeekStart != monday { + t.Error("No populate workweek does not have right values!") + } +} + +func TestCheckStatus(t *testing.T) { + testWeek := SetupWorkWeekFixture(t) + testCases := []struct { + name string + weekStatus models.WeekStatus + }{ + {"State=None", models.WeekStatusNone}, + {"State=Sent", models.WeekStatusSent}, + {"State=Accepted", models.WeekStatusAccepted}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testWeek.Status = tc.weekStatus + if testWeek.CheckStatus() != tc.weekStatus { + t.Error("WorkWeek Status missmatch!") + } + }) + } + +} diff --git a/Backend/sonar-project.properties b/Backend/sonar-project.properties new file mode 100644 index 0000000..d859e77 --- /dev/null +++ b/Backend/sonar-project.properties @@ -0,0 +1,8 @@ +sonar.projectKey=Arbeitszeitmessung +sonar.sources=. +sonar.exclusions=**/*_test.go + +sonar.tests=. +sonar.test.inclusions=**/*_test.go +sonar.go.tests.reportPaths=.test/report.json +sonar.go.coverage.reportPaths=.test/coverage.out diff --git a/Backend/src/main.css b/Backend/src/main.css index 4b3439b..f2b68f0 100644 --- a/Backend/src/main.css +++ b/Backend/src/main.css @@ -1,63 +1,107 @@ @import "tailwindcss"; @source "../templates/*.templ"; +@plugin "@iconify/tailwind4" { + scale: 1.5; +} @theme { - --color-accent-50: #e7fdea; - --color-accent-100: #cbfbd1; - --color-accent-200: #9cf7a8; - --color-accent-300: #68f37a; - --color-accent-400: #33ef4d; - --color-accent-500: #11db2d; - --color-accent-600: #0eaf23; - --color-accent-700: #0a851b; - --color-accent-800: #075a12; - --color-accent-900: #032b09; - --color-accent-950: #021805; - --color-accent: #0eaf23; - --color-text-50: #f7f8f7; - --color-text-100: #f2f3f2; - --color-text-200: #e2e4e2; - --color-text-300: #d2d6d2; - --color-text-400: #c2c7c2; - --color-text-500: #afb6af; - --color-text-600: #97a097; - --color-text-700: #7d877d; - --color-text-800: #5a625a; - --color-text-900: #161816; - --color-text-950: #000000; + --color-accent-50: #e7fdea; + --color-accent-100: #cbfbd1; + --color-accent-200: #9cf7a8; + --color-accent-300: #68f37a; + --color-accent-400: #33ef4d; + --color-accent-500: #11db2d; + --color-accent-600: #0eaf23; + --color-accent-700: #0a851b; + --color-accent-800: #075a12; + --color-accent-900: #032b09; + --color-accent-950: #021805; + --color-accent: #0eaf23; + --color-text-50: #f7f8f7; + --color-text-100: #f2f3f2; + --color-text-200: #e2e4e2; + --color-text-300: #d2d6d2; + --color-text-400: #c2c7c2; + --color-text-500: #afb6af; + --color-text-600: #97a097; + --color-text-700: #7d877d; + --color-text-800: #5a625a; + --color-text-900: #161816; + --color-text-950: #000000; } @layer components { - .grid-main { - display: grid; - grid-template-columns: 2fr auto 1fr; - align-items: stretch; - } - - .grid-sub { - display: grid; - grid-template-columns: subgrid; - grid-column: 1 / -1; - border-color: var(--color-neutral-400); - transition: background-color 0.2s ease-in-out; - } - - .grid-sub:hover { - background-color: var(--color-neutral-200); - } - - .grid-cell { - padding: calc(var(--spacing) * 2); - border-color: var(--color-neutral-400); - } - - @media (width >=48rem) { .grid-main { - grid-template-columns: repeat(5, 1fr); - margin: 0 10%; + display: grid; + grid-template-columns: 2fr auto 1fr; + align-items: stretch; } .grid-sub { + display: grid; + grid-template-columns: subgrid; + grid-column: 1 / -1; + border-color: var(--color-neutral-400); + transition: background-color 0.2s ease-in-out; + } + + .grid-sub.responsive { + display: flex; + flex-direction: column; + } + + .grid-sub:hover { + background-color: var(--color-neutral-200); + } + + .grid-cell { + padding: calc(var(--spacing) * 2); + border-color: var(--color-neutral-400); + } + + .btn { + width: 100%; + cursor: pointer; + border-radius: var(--radius-md); + color: var(--color-neutral-800); + font-size: var(--text-sm); + text-align: center; + padding: calc(var(--spacing) * 2); + border-style: var(--tw-border-style); + border-width: 1px; + border-color: var(--color-neutral-800); + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + + .btn:hover { + color: var(--color-white); + background-color: var(--color-neutral-700); + } + + .btn:disabled { + opacity: 50%; + pointer-events: none; + } + + .btn:active, + .btn:focus { + background-color: var(--color-neutral-700); + } + + @media (width >=48rem) { + .grid-main { + grid-template-columns: repeat(5, 1fr); + margin: 0 10%; + } + + .grid-sub.responsive { + display: grid; + } + + .btn { + padding-inline: calc(var(--spacing) * 4); + } } - } } diff --git a/Backend/static/css/styles.css b/Backend/static/css/styles.css index f67f5de..2f2f5e3 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -1,4 +1,5 @@ -/*! tailwindcss v4.0.8 | MIT License | https://tailwindcss.com */ +/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */ +@layer properties; @layer theme, base, components, utilities; @layer theme { :root, :host { @@ -6,18 +7,18 @@ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - --color-red-500: oklch(0.637 0.237 25.331); - --color-red-600: oklch(0.577 0.245 27.325); - --color-orange-500: oklch(0.705 0.213 47.604); - --color-purple-600: oklch(0.558 0.288 302.321); - --color-neutral-100: oklch(0.97 0 0); - --color-neutral-200: oklch(0.922 0 0); - --color-neutral-300: oklch(0.87 0 0); - --color-neutral-400: oklch(0.708 0 0); - --color-neutral-500: oklch(0.556 0 0); - --color-neutral-700: oklch(0.371 0 0); - --color-neutral-800: oklch(0.269 0 0); - --color-neutral-900: oklch(0.205 0 0); + --color-red-500: oklch(63.7% 0.237 25.331); + --color-red-600: oklch(57.7% 0.245 27.325); + --color-orange-500: oklch(70.5% 0.213 47.604); + --color-purple-600: oklch(55.8% 0.288 302.321); + --color-neutral-100: oklch(97% 0 0); + --color-neutral-200: oklch(92.2% 0 0); + --color-neutral-300: oklch(87% 0 0); + --color-neutral-400: oklch(70.8% 0 0); + --color-neutral-500: oklch(55.6% 0 0); + --color-neutral-700: oklch(37.1% 0 0); + --color-neutral-800: oklch(26.9% 0 0); + --color-neutral-900: oklch(20.5% 0 0); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; @@ -25,22 +26,14 @@ --text-sm--line-height: calc(1.25 / 0.875); --text-xl: 1.25rem; --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); --font-weight-bold: 700; --radius-md: 0.375rem; --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-font-family: var(--font-sans); - --default-font-feature-settings: var(--font-sans--font-feature-settings); - --default-font-variation-settings: var( - --font-sans--font-variation-settings - ); --default-mono-font-family: var(--font-mono); - --default-mono-font-feature-settings: var( - --font-mono--font-feature-settings - ); - --default-mono-font-variation-settings: var( - --font-mono--font-variation-settings - ); --color-accent: #0eaf23; } } @@ -55,14 +48,11 @@ line-height: 1.5; -webkit-text-size-adjust: 100%; tab-size: 4; - font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" ); + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); font-feature-settings: var(--default-font-feature-settings, normal); - font-variation-settings: var( --default-font-variation-settings, normal ); + font-variation-settings: var(--default-font-variation-settings, normal); -webkit-tap-highlight-color: transparent; } - body { - line-height: inherit; - } hr { height: 0; color: inherit; @@ -85,9 +75,9 @@ font-weight: bolder; } code, kbd, samp, pre { - font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace ); - font-feature-settings: var( --default-mono-font-feature-settings, normal ); - font-variation-settings: var( --default-mono-font-variation-settings, normal ); + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); font-size: 1em; } small { @@ -151,7 +141,14 @@ } ::placeholder { opacity: 1; - color: color-mix(in oklab, currentColor 50%, transparent); + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } } textarea { resize: vertical; @@ -172,6 +169,9 @@ ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { padding-block: 0; } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } :-moz-ui-invalid { box-shadow: none; } @@ -186,8 +186,8 @@ } } @layer utilities { - .static { - position: static; + .\@container { + container-type: inline-size; } .col-span-2 { grid-column: span 2 / span 2; @@ -195,6 +195,9 @@ .col-span-3 { grid-column: span 3 / span 3; } + .col-span-full { + grid-column: 1 / -1; + } .mx-auto { margin-inline: auto; } @@ -207,9 +210,77 @@ .mb-2 { margin-bottom: calc(var(--spacing) * 2); } + .icon-\[material-symbols-light--add-circle-outline\] { + display: inline-block; + width: 1.5em; + height: 1.5em; + 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='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"); + } + .icon-\[material-symbols-light--check-circle-outline\] { + display: inline-block; + width: 1.5em; + height: 1.5em; + 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='m10.562 15.908l6.396-6.396l-.708-.708l-5.688 5.688l-2.85-2.85l-.708.708zM12.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--circle-outline\] { + display: inline-block; + width: 1.5em; + height: 1.5em; + 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='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--more-time\] { + display: inline-block; + width: 1.5em; + height: 1.5em; + 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='M11.003 20q-1.666 0-3.123-.622t-2.545-1.71t-1.712-2.544T3 12.003t.622-3.123t1.711-2.546q1.09-1.089 2.545-1.711T11 4q.525 0 1.013.063T13 4.25V5.3q-.5-.15-.987-.225T11 5Q8.089 5 6.044 7.044T4 12t2.044 4.956T11 19t4.956-2.044T18 11.996q0-.271-.025-.554t-.094-.557h1.011q.05.236.08.538q.028.302.028.577q0 1.667-.622 3.122t-1.71 2.545q-1.089 1.088-2.544 1.71q-1.455.623-3.121.623m3.143-4.146L10.5 12.208V7h1v4.792l3.354 3.354zM18 8.884v-3h-3v-1h3v-3h1v3h3v1h-3v3z'/%3E%3C/svg%3E"); + } + .icon-\[material-symbols-light--motion-photos-paused-outline\] { + display: inline-block; + width: 1.5em; + height: 1.5em; + 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='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"); + } .flex { display: flex; } + .grid { + display: grid; + } .hidden { display: none; } @@ -239,9 +310,6 @@ .h-full { height: 100%; } - .w-1\/3 { - width: calc(1/3 * 100%); - } .w-2 { width: calc(var(--spacing) * 2); } @@ -254,9 +322,15 @@ .w-\[2px\] { width: 2px; } + .w-auto { + width: auto; + } .w-full { width: 100%; } + .flex-shrink-0 { + flex-shrink: 0; + } .flex-grow { flex-grow: 1; } @@ -272,12 +346,18 @@ .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .grid-cols-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } .flex-col { flex-direction: column; } .flex-row { flex-direction: row; } + .content-baseline { + align-content: baseline; + } .content-end { align-content: flex-end; } @@ -319,11 +399,6 @@ .justify-self-end { justify-self: flex-end; } - .truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } .overflow-hidden { overflow: hidden; } @@ -343,9 +418,6 @@ .border-neutral-300 { border-color: var(--color-neutral-300); } - .border-neutral-800 { - border-color: var(--color-neutral-800); - } .border-neutral-900 { border-color: var(--color-neutral-900); } @@ -382,9 +454,16 @@ .py-2 { padding-block: calc(var(--spacing) * 2); } + .py-4 { + padding-block: calc(var(--spacing) * 4); + } .text-center { text-align: center; } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } .text-sm { font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)); @@ -425,7 +504,7 @@ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } .transition { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } @@ -505,6 +584,13 @@ } } } + .hover\:bg-red-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-600); + } + } + } .hover\:text-accent { &:hover { @media (hover: hover) { @@ -560,11 +646,38 @@ display: grid; } } + .max-md\:hidden { + @media (width < 48rem) { + display: none; + } + } .max-md\:flex-col { @media (width < 48rem) { flex-direction: column; } } + .max-md\:divide-y-1 { + @media (width < 48rem) { + :where(& > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(1px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + } + } + } + .max-md\:border-b-1 { + @media (width < 48rem) { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + } + .max-md\:bg-neutral-300 { + @media (width < 48rem) { + background-color: var(--color-neutral-300); + } + } .md\:col-span-1 { @media (width >= 48rem) { grid-column: span 1 / span 1; @@ -622,6 +735,36 @@ } } } + .lg\:hidden { + @media (width >= 64rem) { + display: none; + } + } + .lg\:grid-cols-1 { + @media (width >= 64rem) { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + } + .lg\:divide-x-1 { + @media (width >= 64rem) { + :where(& > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); + } + } + } + .\@7xl\:grid { + @container (width >= 80rem) { + display: grid; + } + } + .\@7xl\:grid-cols-5 { + @container (width >= 80rem) { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } + } .print\:hidden { @media print { display: none; @@ -641,6 +784,10 @@ border-color: var(--color-neutral-400); transition: background-color 0.2s ease-in-out; } + .grid-sub.responsive { + display: flex; + flex-direction: column; + } .grid-sub:hover { background-color: var(--color-neutral-200); } @@ -648,11 +795,43 @@ padding: calc(var(--spacing) * 2); border-color: var(--color-neutral-400); } + .btn { + width: 100%; + cursor: pointer; + border-radius: var(--radius-md); + color: var(--color-neutral-800); + font-size: var(--text-sm); + text-align: center; + padding: calc(var(--spacing) * 2); + border-style: var(--tw-border-style); + border-width: 1px; + border-color: var(--color-neutral-800); + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .btn:hover { + color: var(--color-white); + background-color: var(--color-neutral-700); + } + .btn:disabled { + opacity: 50%; + pointer-events: none; + } + .btn:active, .btn:focus { + background-color: var(--color-neutral-700); + } @media (width >=48rem) { .grid-main { grid-template-columns: repeat(5, 1fr); margin: 0 10%; } + .grid-sub.responsive { + display: grid; + } + .btn { + padding-inline: calc(var(--spacing) * 4); + } } } @property --tw-divide-x-reverse { @@ -714,7 +893,44 @@ syntax: "*"; inherits: false; } +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} @property --tw-duration { syntax: "*"; inherits: false; } +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-divide-x-reverse: 0; + --tw-border-style: solid; + --tw-divide-y-reverse: 0; + --tw-font-weight: initial; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-duration: initial; + } + } +} diff --git a/Backend/static/script.js b/Backend/static/script.js index 5bc2abb..520ec52 100644 --- a/Backend/static/script.js +++ b/Backend/static/script.js @@ -1,7 +1,5 @@ function editDay(element, event, formId) { - var form = element - .closest(".grid-sub") - .querySelector(".all-booking-component > form"); + var form = element.closest(".grid-sub").querySelector(".all-booking-component > form"); form.classList.toggle("edit"); element.classList.toggle("edit"); if (element.classList.contains("edit")) { @@ -15,9 +13,7 @@ function editDay(element, event, formId) { } function editAbwesenheit(element, event) { - var newBookingComponent = element - .closest(".grid-sub") - .querySelector(".new-booking-component"); + var newBookingComponent = element.closest(".grid-sub").querySelector(".new-booking-component"); if (element.value == 0) { newBookingComponent.style.display = ""; } else { @@ -32,3 +28,7 @@ function navigateWeek(element, event, direction) { date.setHours(10); dateInput.valueAsDate = date; } + +function logoutUser() { + fetch("/user/logout", {}).then(() => window.location.reload()); +} diff --git a/Backend/templates/headerComponent.templ b/Backend/templates/headerComponent.templ index bfb040f..ec2d673 100644 --- a/Backend/templates/headerComponent.templ +++ b/Backend/templates/headerComponent.templ @@ -1,14 +1,13 @@ package templates - - templ headerComponent() { -
+
Zeitverwaltung Abrechnung if true { Anwesenheit } Einstellungen + @LogoutButton()
} diff --git a/Backend/templates/headerComponent_templ.go b/Backend/templates/headerComponent_templ.go index 4710b0a..aeef6ec 100644 --- a/Backend/templates/headerComponent_templ.go +++ b/Backend/templates/headerComponent_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.833 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -29,7 +29,7 @@ func headerComponent() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Zeitverwaltung Abrechnung ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Zeitverwaltung Abrechnung ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -39,7 +39,15 @@ func headerComponent() templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Einstellungen
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Einstellungen") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = LogoutButton().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/pages.templ b/Backend/templates/pages.templ index 73d9785..7915162 100644 --- a/Backend/templates/pages.templ +++ b/Backend/templates/pages.templ @@ -1,8 +1,10 @@ package templates import ( + "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "fmt" + "log" "strconv" "time" ) @@ -17,13 +19,13 @@ templ Base() { } -templ TimePage(workDays []models.WorkDay) { +templ TimePage(workDays []models.WorkDay, lastSub time.Time) { @Base() @headerComponent()
@inputForm() for _, day := range workDays { - @dayComponent(day) + @dayComponent(day, day.Day.Before(lastSub)) }
@LegendComponent() @@ -48,8 +50,7 @@ templ UserPage(status int) { @Base() @headerComponent()
-
-
+

Passwort ändern

@@ -65,101 +66,103 @@ templ UserPage(status int) { }
- +
-
+

Nutzer abmelden

Nutzer von Weboberfläche abmelden.

- +
- +
} +templ statusCheckMark(status models.WeekStatus, target models.WeekStatus) { + if status >= target { +
+ } else { +
+ } +} + templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) { - {{ - year, kw := userWeek.WeekStart.ISOWeek() - }} @Base() @headerComponent() + {{ + progress := (float32(userWeek.Worktime.Hours()) / userWeek.User.ArbeitszeitPerWoche) * 100 + log.Println(userWeek.CheckStatus()) + }}
-
-
{ fmt.Sprintf("%s %s", userWeek.User.Vorname, userWeek.User.Name) }
-
+
+
+

Eigene Abrechnung

+
+
+
+ @weekPicker(userWeek.WeekStart) +
+

{ fmt.Sprintf("%s %s", userWeek.User.Vorname, userWeek.User.Name) }

+
+
+ + @statusCheckMark(userWeek.CheckStatus(), models.WeekStatusSent) + Gesendet + + + @statusCheckMark(userWeek.CheckStatus(), models.WeekStatusAccepted) + Akzeptiert + +
+
+ @timeGaugeComponent(uint8(progress), false, false) +
+

Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDuration(userWeek.Worktime)) }

+

Überstunden: { fmt.Sprintf("%s", helper.FormatDuration(userWeek.Overtime)) }

+
+
+
+
+
for _, day := range userWeek.WorkDays { @weekDayComponent(userWeek.User, day) }
-
-
- - -

KW { fmt.Sprintf("%02d, %d", kw, year) }

- -
-
+
+
+ @weekPicker(userWeek.WeekStart) +
+ switch userWeek.CheckStatus() { case models.WeekStatusNone:

an Vorgesetzten senden

- - if time.Since(userWeek.WeekStart) < 24*7*time.Hour { -

Die Woche kann erst am nächsten Montag abgesendet werden!

- } case models.WeekStatusSent:

an Vorgesetzten gesendet

- -

- akzeptiert: - - - -

case models.WeekStatusAccepted:

vom Vorgesetzten bestätigt

- -

- akzeptiert: - - - - -

} + +
+ if len(weeks) > 0 { +
+

Abrechnung Mitarbeiter

+
+ } for _, week := range weeks { @employeComponent(week) }
} -templ NavPage() { - @Base() - -} - templ TeamPresencePage(teamPresence map[bool][]models.User) { @Base() @headerComponent() @@ -182,3 +185,7 @@ templ TeamPresencePage(teamPresence map[bool][]models.User) {
} + +templ LogoutButton() { + +} diff --git a/Backend/templates/pages_templ.go b/Backend/templates/pages_templ.go index c649f96..346d818 100644 --- a/Backend/templates/pages_templ.go +++ b/Backend/templates/pages_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.833 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -9,8 +9,10 @@ import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" import ( + "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "fmt" + "log" "strconv" "time" ) @@ -44,7 +46,7 @@ func Base() templ.Component { }) } -func TimePage(workDays []models.WorkDay) templ.Component { +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 { @@ -82,7 +84,7 @@ func TimePage(workDays []models.WorkDay) templ.Component { return templ_7745c5c3_Err } for _, day := range workDays { - templ_7745c5c3_Err = dayComponent(day).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = dayComponent(day, day.Day.Before(lastSub)).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -171,7 +173,7 @@ func UserPage(status int) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

Passwort ändern

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

Passwort ändern

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -192,7 +194,7 @@ func UserPage(status int) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

Nutzer abmelden

Nutzer von Weboberfläche abmelden.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

Nutzer abmelden

Nutzer von Weboberfläche abmelden.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -200,6 +202,42 @@ func UserPage(status int) templ.Component { }) } +func statusCheckMark(status models.WeekStatus, target models.WeekStatus) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if status >= target { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context @@ -216,13 +254,11 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var5 := templ.GetChildren(ctx) - if templ_7745c5c3_Var5 == nil { - templ_7745c5c3_Var5 = templ.NopComponent + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - - year, kw := userWeek.WeekStart.ISOWeek() templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -231,20 +267,81 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + + progress := (float32(userWeek.Worktime.Hours()) / userWeek.User.ArbeitszeitPerWoche) * 100 + log.Println(userWeek.CheckStatus()) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

Eigene Abrechnung

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s %s", userWeek.User.Vorname, userWeek.User.Name)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 91, Col: 111} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + 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, 13, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

") + 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, "

") + 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 ") + 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
") + 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, "

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, "

Ü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, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -254,183 +351,98 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, 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: 99, Col: 110} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + 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, 15, "\"> ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "

KW ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d, %d", kw, year)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 105, Col: 72} - } - _, 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, 18, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "1")) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + 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, 25, "

an Vorgesetzten senden

an Vorgesetzten senden

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if time.Since(userWeek.WeekStart) < 24*7*time.Hour { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " disabled") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " type=\"submit\" class=\"w-full cursor-pointer rounded-md text-neutral-800 p-2 md:px-4 border text-center text-sm hover:text-white transition-colors border-neutral-800 focus:bg-neutral-700 active:bg-neutral-700 hover:bg-neutral-700 disabled:pointer-events-none disabled:opacity-50\">Senden ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if time.Since(userWeek.WeekStart) < 24*7*time.Hour { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

Die Woche kann erst am nächsten Montag abgesendet werden!

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } case models.WeekStatusSent: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "

an Vorgesetzten gesendet

akzeptiert:

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "

an Vorgesetzten gesendet

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case models.WeekStatusAccepted: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "

vom Vorgesetzten bestätigt

akzeptiert:

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

vom Vorgesetzten bestätigt

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "Korrigieren = 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
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(weeks) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

Abrechnung Mitarbeiter

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } for _, week := range weeks { templ_7745c5c3_Err = employeComponent(week).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func NavPage() templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var13 := templ.GetChildren(ctx) - if templ_7745c5c3_Var13 == nil { - templ_7745c5c3_Var13 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -454,9 +466,9 @@ func TeamPresencePage(teamPresence map[bool][]models.User) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var14 := templ.GetChildren(ctx) - if templ_7745c5c3_Var14 == nil { - templ_7745c5c3_Var14 = templ.NopComponent + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) @@ -467,7 +479,7 @@ func TeamPresencePage(teamPresence map[bool][]models.User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

Anwesend

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "

Anwesend

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -477,7 +489,7 @@ func TeamPresencePage(teamPresence map[bool][]models.User) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "

Nicht Anwesend

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "

Nicht Anwesend

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -487,7 +499,36 @@ func TeamPresencePage(teamPresence map[bool][]models.User) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func LogoutButton() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/teamComponents.templ b/Backend/templates/teamComponents.templ index 40679c5..a43a3f6 100644 --- a/Backend/templates/teamComponents.templ +++ b/Backend/templates/teamComponents.templ @@ -1,12 +1,37 @@ package templates import ( + "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "fmt" + "log" "strconv" "time" ) +templ weekPicker(weekStart time.Time) { + {{ + year, kw := weekStart.ISOWeek() + }} +
+ + +

KW { fmt.Sprintf("%02d, %d", kw, year) }

+ +
+ if time.Since(weekStart) < 24*7*time.Hour { +

Die Woche kann erst am nächsten Montag gesendet werden!

+ } +} + templ weekDayComponent(user models.User, day models.WorkDay) { {{ work, pause := day.GetWorkTimeString() }}
@@ -22,12 +47,14 @@ templ weekDayComponent(user models.User, day models.WorkDay) { - if day.TimeFrom == day.TimeTo { -

Keine Anwesenheit

- } else { + if day.Absence.Datum.Equal(day.Day) { +

{ day.Absence.AbwesenheitTyp.Name }

+ } else if !day.TimeFrom.Equal(day.TimeTo) { { day.TimeFrom.Format("15:04") } - { day.TimeTo.Format("15:04") } + } else { +

Keine Anwesenheit

}
@@ -37,14 +64,21 @@ templ weekDayComponent(user models.User, day models.WorkDay) { templ employeComponent(week models.WorkWeek) { {{ year, kw := week.WeekStart.ISOWeek() + progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100 + log.Println(progress) }} -
-
+
+

{ week.User.Vorname } { week.User.Name }

-

Arbeitszeit

-

{ week.GetWorkHourString() }

+
+ @timeGaugeComponent(uint8(progress), false, false) +
+

Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDuration(week.Worktime)) }

+

Überstunden: { fmt.Sprintf("%s", helper.FormatDuration(week.Overtime)) }

+
+
-
+
for _, day := range week.WorkDays { @weekDayComponent(week.User, day) } @@ -54,9 +88,14 @@ templ employeComponent(week models.WorkWeek) { - +
+ if week.Status == models.WeekStatusDifferences { +

Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen

+ } + + // TODO maybe delete function + // +
} diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index efd4951..abe699c 100644 --- a/Backend/templates/teamComponents_templ.go +++ b/Backend/templates/teamComponents_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.833 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -9,13 +9,15 @@ import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" import ( + "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "fmt" + "log" "strconv" "time" ) -func weekDayComponent(user models.User, day models.WorkDay) templ.Component { +func weekPicker(weekStart time.Time) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -36,8 +38,115 @@ func weekDayComponent(user models.User, day models.WorkDay) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) + + year, kw := weekStart.ISOWeek() + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "-1")) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

KW ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d, %d", kw, year)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 23, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("navigateWeek", templ.JSExpression("this"), templ.JSExpression("event"), "1")) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if time.Since(weekStart) < 24*7*time.Hour { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

Die Woche kann erst am nächsten Montag gesendet werden!

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +func weekDayComponent(user models.User, day models.WorkDay) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) work, pause := day.GetWorkTimeString() - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -45,100 +154,118 @@ func weekDayComponent(user models.User, day models.WorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ": ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var3 string - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(day.Day.Format("02.01.2006")) + 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: 15, Col: 130} + 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_Var3)) + _, 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, 4, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var4 string - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(work) + 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: 17, Col: 36} + 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_Var4)) + _, 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, 5, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var5 string - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(pause) + 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: 18, Col: 42} + 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_Var5)) + _, 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, 6, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if day.TimeFrom == day.TimeTo { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

Keine Anwesenheit

") + if day.Absence.Datum.Equal(day.Day) { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

") + 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, "

") + 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, "") + 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, " - ") + 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, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, 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: 28, Col: 41} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " - ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, 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: 30, Col: 39} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

Keine Anwesenheit

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -162,53 +289,76 @@ func employeComponent(week models.WorkWeek) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var8 := templ.GetChildren(ctx) - if templ_7745c5c3_Var8 == nil { - templ_7745c5c3_Var8 = templ.NopComponent + templ_7745c5c3_Var14 := templ.GetChildren(ctx) + if templ_7745c5c3_Var14 == nil { + templ_7745c5c3_Var14 = templ.NopComponent } ctx = templ.ClearChildren(ctx) year, kw := week.WeekStart.ISOWeek() - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

") + progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100 + log.Println(progress) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Vorname) + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Vorname) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 43, Col: 53} + 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_Var9)) + _, 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, 13, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Name) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 43, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 72, Col: 72} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + _, 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, 14, "

Arbeitszeit

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(week.GetWorkHourString()) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 45, Col: 52} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + 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, 15, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "

Arbeitszeit: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDuration(week.Worktime))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 76, Col: 78} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

Überstunden: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDuration(week.Overtime))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 77, Col: 79} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -218,46 +368,66 @@ func employeComponent(week models.WorkWeek) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

Woche: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "

Woche: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d-%d", kw, year)) + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d-%d", kw, year)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 53, Col: 85} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 87, Col: 85} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + _, 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, 17, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if week.Status == models.WeekStatusDifferences { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen

") + 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 } @@ -281,53 +451,53 @@ func userPresenceComponent(user models.User, present bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var15 := templ.GetChildren(ctx) - if templ_7745c5c3_Var15 == nil { - templ_7745c5c3_Var15 = templ.NopComponent + templ_7745c5c3_Var22 := templ.GetChildren(ctx) + if templ_7745c5c3_Var22 == nil { + templ_7745c5c3_Var22 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if present { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
Anwesend
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
Anwesend
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
Abwesend
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
Abwesend
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname) + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 71, Col: 19} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 110, Col: 19} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, 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, 24, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 71, Col: 33} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 110, Col: 33} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, 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, 25, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/timeComponents.templ b/Backend/templates/timeComponents.templ index 9a7b68a..3b89eae 100644 --- a/Backend/templates/timeComponents.templ +++ b/Backend/templates/timeComponents.templ @@ -1,6 +1,7 @@ package templates import ( + "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "fmt" "net/url" @@ -18,7 +19,7 @@ templ inputForm() {

{ user.Vorname + " " + user.Name }

Überstunden

-

0h 0min (statisch)

+

{ user.Overtime }

@@ -29,22 +30,24 @@ templ inputForm() {
-
} -templ dayComponent(workDay models.WorkDay) { +templ dayComponent(workDay models.WorkDay, submitted bool) { {{ work, pause := workDay.GetWorkTimeString() + user := ctx.Value("user").(models.User) + overtime := helper.FormatDuration(workDay.CalcOvertime(user)) justify := "" if len(workDay.Bookings) <= 1 { justify = "justify-content: center" } }} -
+
@timeGaugeComponent(workDay.GetWorkDayProgress(ctx.Value("user").(models.User)), workDay.Day.Equal(time.Now().Truncate(24*time.Hour)), workDay.RequiresAction())
@@ -56,7 +59,10 @@ templ dayComponent(workDay models.WorkDay) { } else {

{ work }

} -

{ pause }

+

{ pause }

+ if overtime != "" { +

{ overtime }

+ } }
@@ -64,7 +70,7 @@ templ dayComponent(workDay models.WorkDay) { @lineComponent()
if (workDay.Absence != models.Absence{}) { -

{ workDay.Absence.GetStringType() }

+

{ workDay.Absence.AbwesenheitTyp.Name }

} if len(workDay.Bookings) < 1 && (workDay.Absence == models.Absence{}) {

Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!

@@ -87,7 +93,7 @@ templ dayComponent(workDay models.WorkDay) { } templ changeButtonComponent(id string) { -
} else { -
+
} } @@ -143,8 +149,8 @@ templ absenceComponent(d models.WorkDay) { @@ -152,12 +158,7 @@ templ absenceComponent(d models.WorkDay) { templ newBookingComponent(d models.WorkDay) {
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" 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...\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -95,7 +109,7 @@ func inputForm() templ.Component { }) } -func dayComponent(workDay models.WorkDay) templ.Component { +func dayComponent(workDay models.WorkDay, submitted 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 { @@ -111,18 +125,38 @@ func dayComponent(workDay models.WorkDay) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var5 := templ.GetChildren(ctx) - if templ_7745c5c3_Var5 == nil { - templ_7745c5c3_Var5 = templ.NopComponent + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent } ctx = templ.ClearChildren(ctx) work, pause := workDay.GetWorkTimeString() + user := ctx.Value("user").(models.User) + overtime := helper.FormatDuration(workDay.CalcOvertime(user)) justify := "" if len(workDay.Bookings) <= 1 { justify = "justify-content: center" } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + var templ_7745c5c3_Var7 = []any{"grid-sub divide-x-1 hover:bg-neutral-200 transition-colors", templ.KV("bg-neutral-100", submitted)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -130,84 +164,103 @@ func dayComponent(workDay models.WorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ": ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Day.Format("02.01.2006")) + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Day.Format("02.01.2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 51, Col: 139} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 54, Col: 139} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + _, 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, 9, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if work != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

Arbeitszeit:

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

Arbeitszeit:

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if workDay.RequiresAction() { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

Bitte anpassen

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

Bitte anpassen

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(work) + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(work) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 57, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 60, Col: 36} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + _, 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, 13, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(pause) + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(pause) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 59, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 62, Col: 148} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + if overtime != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(overtime) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 64, Col: 133} + } + _, 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -215,57 +268,57 @@ func dayComponent(workDay models.WorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" method=\"post\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if (workDay.Absence != models.Absence{}) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Absence.GetStringType()) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Absence.AbwesenheitTyp.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 67, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 73, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + _, 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, 21, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(workDay.Bookings) < 1 && (workDay.Absence == models.Absence{}) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -273,7 +326,7 @@ func dayComponent(workDay models.WorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -292,7 +345,7 @@ func dayComponent(workDay models.WorkDay) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -301,7 +354,7 @@ func dayComponent(workDay models.WorkDay) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -309,7 +362,7 @@ func dayComponent(workDay models.WorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -333,25 +386,25 @@ func changeButtonComponent(id string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var13 := templ.GetChildren(ctx) - if templ_7745c5c3_Var13 == nil { - templ_7745c5c3_Var13 = templ.NopComponent + templ_7745c5c3_Var17 := templ.GetChildren(ctx) + if templ_7745c5c3_Var17 == nil { + templ_7745c5c3_Var17 = templ.NopComponent } ctx = templ.ClearChildren(ctx) templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("editDay", templ.JSExpression("this"), templ.JSExpression("event"), id)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">

Ändern

Absenden

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -375,9 +428,9 @@ func timeGaugeComponent(progress uint8, today bool, warning bool) templ.Componen }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var15 := templ.GetChildren(ctx) - if templ_7745c5c3_Var15 == nil { - templ_7745c5c3_Var15 = templ.NopComponent + templ_7745c5c3_Var19 := templ.GetChildren(ctx) + if templ_7745c5c3_Var19 == nil { + templ_7745c5c3_Var19 = templ.NopComponent } ctx = templ.ClearChildren(ctx) @@ -386,13 +439,13 @@ func timeGaugeComponent(progress uint8, today bool, warning bool) templ.Componen case (warning): bgColor = "bg-red-600" break - case (progress > 0 && progress < 90): + case (progress > 0 && progress < 95): bgColor = "bg-orange-500" break - case (90 <= progress && progress <= 110): + case (95 <= progress && progress <= 105): bgColor = "bg-accent" break - case (progress > 110): + case (progress > 105): bgColor = "bg-purple-600" break default: @@ -400,65 +453,65 @@ func timeGaugeComponent(progress uint8, today bool, warning bool) templ.Componen break } if today { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 = []any{"flex w-full items-center justify-center overflow-hidden rounded-full", bgColor} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...) + var templ_7745c5c3_Var20 = []any{"flex w-full items-center justify-center overflow-hidden rounded-full", bgColor} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - var templ_7745c5c3_Var19 = []any{"w-2 h-full bg-accent rounded-md", bgColor} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...) + var templ_7745c5c3_Var23 = []any{"w-2 h-full bg-accent rounded-md flex-shrink-0", bgColor} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -483,12 +536,12 @@ func lineComponent() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var21 := templ.GetChildren(ctx) - if templ_7745c5c3_Var21 == nil { - templ_7745c5c3_Var21 = templ.NopComponent + templ_7745c5c3_Var25 := templ.GetChildren(ctx) + if templ_7745c5c3_Var25 == nil { + templ_7745c5c3_Var25 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -512,12 +565,12 @@ func absenceComponent(d models.WorkDay) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var22 := templ.GetChildren(ctx) - if templ_7745c5c3_Var22 == nil { - templ_7745c5c3_Var22 = templ.NopComponent + templ_7745c5c3_Var26 := templ.GetChildren(ctx) + if templ_7745c5c3_Var26 == nil { + templ_7745c5c3_Var26 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -594,58 +647,58 @@ func newBookingComponent(d models.WorkDay) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var26 := templ.GetChildren(ctx) - if templ_7745c5c3_Var26 == nil { - templ_7745c5c3_Var26 = templ.NopComponent + templ_7745c5c3_Var30 := templ.GetChildren(ctx) + if templ_7745c5c3_Var30 == nil { + templ_7745c5c3_Var30 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, ">Gehen") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -669,64 +722,64 @@ func bookingComponent(booking models.Booking) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var29 := templ.GetChildren(ctx) - if templ_7745c5c3_Var29 == nil { - templ_7745c5c3_Var29 = templ.NopComponent + templ_7745c5c3_Var33 := templ.GetChildren(ctx) + if templ_7745c5c3_Var33 == nil { + templ_7745c5c3_Var33 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(booking.Timestamp.Format("15:04")) + var templ_7745c5c3_Var34 string + templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(booking.Timestamp.Format("15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 174, Col: 97} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 175, Col: 97} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" 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\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var33 string - templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(booking.GetBookingType()) + var templ_7745c5c3_Var37 string + templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(booking.GetBookingType()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 176, Col: 29} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 177, Col: 29} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -750,12 +803,12 @@ func LegendComponent() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var34 := templ.GetChildren(ctx) - if templ_7745c5c3_Var34 == nil { - templ_7745c5c3_Var34 = templ.NopComponent + templ_7745c5c3_Var38 := templ.GetChildren(ctx) + if templ_7745c5c3_Var38 == nil { + templ_7745c5c3_Var38 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
Fehler
Arbeitszeit unter regulär
Arbeitszeit vollständig
Überstunden
Keine Buchungen
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
Fehler
Arbeitszeit unter regulär
Arbeitszeit vollständig
Überstunden
Keine Buchungen
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/DB/initdb/01_create_tables.sql b/DB/initdb/01_schema.sql similarity index 68% rename from DB/initdb/01_create_tables.sql rename to DB/initdb/01_schema.sql index 4cac87c..14b71ad 100644 --- a/DB/initdb/01_create_tables.sql +++ b/DB/initdb/01_schema.sql @@ -3,20 +3,33 @@ -- ---------------------------- DROP TABLE IF EXISTS "anwesenheit"; CREATE TABLE "anwesenheit" ( - "counter_id" bigserial PRIMARY KEY, - "timestamp" timestamptz(6) DEFAULT CURRENT_TIMESTAMP, - "card_uid" varchar(255), - "check_in_out" int2, - "geraet_id" int2 + "counter_id" bigserial NOT NULL, + "timestamp" timestamptz NULL DEFAULT CURRENT_TIMESTAMP, + "card_uid" character varying(255) NOT NULL, + "check_in_out" smallint NOT NULL, + "geraet_id" smallint NULL, + "anwesenheit_typ" int2, + PRIMARY KEY ("counter_id") ); + COMMENT ON COLUMN "anwesenheit"."check_in_out" IS '1=Check In 2=Check Out , 3=Check in Manuell, 4=Check out manuell255=Automatic Check Out'; COMMENT ON COLUMN "anwesenheit"."geraet_id" IS 'ID des Lesegerätes'; +-- ---------------------------- +-- Table structure for anwesenheitstypen +-- ---------------------------- + +DROP TABLE IF EXISTS "s_anwesenheit_typen"; +CREATE TABLE "s_anwesenheit_typen" ( + "anwesenheit_id" int2 PRIMARY KEY, + "anwesenheit_name" varchar(255) +); + -- ---------------------------- -- Table structure for personal_daten -- ---------------------------- -DROP TABLE IF EXISTS "personal_daten"; -CREATE TABLE "personal_daten" ( +DROP TABLE IF EXISTS "s_personal_daten"; +CREATE TABLE "s_personal_daten" ( "personal_nummer" int4 NOT NULL PRIMARY KEY, "aktiv_beschaeftigt" bool, "vorname" varchar(255), @@ -28,11 +41,12 @@ CREATE TABLE "personal_daten" ( "card_uid" varchar(255), "hauptbeschaeftigungs_ort" int2, "arbeitszeit_per_tag" float4, + "arbeitszeit_per_woche" float4, "arbeitszeit_min_start" time(6), "arbeitszeit_max_ende" time(6), "vorgesetzter_pers_nr" int4 ); -COMMENT ON COLUMN "personal_daten"."geschlecht" IS '1==weiblich, 2==maennlich, 3==divers'; +COMMENT ON COLUMN "s_personal_daten"."geschlecht" IS '1==weiblich, 2==maennlich, 3==divers'; DROP TABLE IF EXISTS "user_password"; CREATE TABLE "user_password" ( @@ -67,6 +81,8 @@ CREATE TABLE "wochen_report" ( "personal_nummer" int4, "woche_start" date, "bestaetigt" bool DEFAULT FALSE, + "arbeitszeit" float4, + "ueberstunden" float4, UNIQUE ("personal_nummer", "woche_start") ); @@ -78,15 +94,13 @@ CREATE TABLE "abwesenheit" ( "datum" timestamptz(6) DEFAULT NOW()::DATE ); + DROP TABLE IF EXISTS "s_abwesenheit_typen"; +CREATE TABLE "s_abwesenheit_typen" ( + "abwesenheit_id" int2 PRIMARY KEY, + "abwesenheit_name" varchar(255), + "arbeitszeit_equivalent" float4 +); + -- Adds crypto extension CREATE EXTENSION IF NOT EXISTS pgcrypto; - - --- Insert into personal_daten - -INSERT INTO "personal_daten" ("personal_nummer", "aktiv_beschaeftigt", "vorname", "nachname", "geburtsdatum", "plz", "adresse", "geschlecht", "card_uid", "hauptbeschaeftigungs_ort", "arbeitszeit_per_tag", "arbeitszeit_min_start", "arbeitszeit_max_ende", "vorgesetzter_pers_nr") VALUES -(123, 't', 'Max', 'Mustermann', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'acde-edca', 1, 7.5, '07:00:00', '20:00:00', 0); - -INSERT INTO "user_password" ("personal_nummer", "pass_hash") VALUES -(123, crypt('max_pass', gen_salt('bf'))); diff --git a/DB/initdb/02_sample_data.sql b/DB/initdb/02_sample_data.sql new file mode 100644 index 0000000..5cf0910 --- /dev/null +++ b/DB/initdb/02_sample_data.sql @@ -0,0 +1,8 @@ +INSERT INTO "s_personal_daten" ("personal_nummer", "aktiv_beschaeftigt", "vorname", "nachname", "geburtsdatum", "plz", "adresse", "geschlecht", "card_uid", "hauptbeschaeftigungs_ort", "arbeitszeit_per_tag", "arbeitszeit_min_start", "arbeitszeit_max_ende", "vorgesetzter_pers_nr") VALUES +(123, 't', 'Kim', 'Mustermensch', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'aaaa-aaaa', 1, 8, '07:00:00', '20:00:00', 0); + +INSERT INTO "user_password" ("personal_nummer", "pass_hash") VALUES +(123, crypt('max_pass', gen_salt('bf'))); + +INSERT INTO "s_anwesenheit_typen" ("anwesenheit_id", "anwesenheit_name") VALUES (1, 'Büro'); +INSERT INTO "s_abwesenheit_typen" ("abwesenheit_id", "abwesenheit_name", "arbeitszeit_equivalent") VALUES (1, 'Urlaub', 10), (2, 'Krank', 10), (3, 'Kurzarbeit', 2); diff --git a/DB/initdb/02_create_user.sh b/DB/initdb/03_create_user.sh similarity index 74% rename from DB/initdb/02_create_user.sh rename to DB/initdb/03_create_user.sh index 160c76e..c850d00 100644 --- a/DB/initdb/02_create_user.sh +++ b/DB/initdb/03_create_user.sh @@ -7,7 +7,8 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E CREATE USER $POSTGRES_API_USER WITH ENCRYPTED PASSWORD '$POSTGRES_API_PASS'; GRANT CONNECT ON DATABASE $POSTGRES_DB TO $POSTGRES_API_USER; GRANT USAGE ON SCHEMA public TO $POSTGRES_API_USER; - GRANT SELECT, INSERT, UPDATE ON anwesenheit, abwesenheit, personal_daten, user_password, wochen_report TO $POSTGRES_API_USER; + GRANT SELECT, INSERT, UPDATE ON anwesenheit, abwesenheit, user_password, wochen_report TO $POSTGRES_API_USER; + GRANT SELECT ON s_personal_daten, s_abwesenheit_typen, s_anwesenheit_typen TO $POSTGRES_API_USER; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO $POSTGRES_API_USER; EOSQL diff --git a/Docker/.env.example b/Docker/.env.example index 8eea0f3..256e380 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -1,7 +1,7 @@ POSTGRES_USER=root POSTGRES_PASSWORD=very_secure -POSTGRES_API_USER=api_nuter -POSTGRES_API_PASSWORD=password +POSTGRES_API_USER=api_nutzer +POSTGRES_API_PASS=password POSTGRES_PATH=../DB POSTGRES_DB=arbeitszeitmessung EXPOSED_PORT=8000 diff --git a/Docker/docker-compose.dev.yml b/Docker/docker-compose.dev.yml index 2abeb41..dd75faf 100644 --- a/Docker/docker-compose.dev.yml +++ b/Docker/docker-compose.dev.yml @@ -9,7 +9,7 @@ services: PGDATA: /var/lib/postgresql/data/pg_data volumes: - ${POSTGRES_PATH}:/var/lib/postgresql/data - - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d + # - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d ports: - 5432:5432 @@ -19,8 +19,7 @@ services: ports: - 8001:8080 backend: - build: ../Backend - image: git.letsstein.de/tom/arbeitszeit-backend:0.1.1 + image: git.letsstein.de/tom/arbeitszeitmessung restart: unless-stopped env_file: - .env diff --git a/Docker/docker-compose.test.yml b/Docker/docker-compose.test.yml new file mode 100644 index 0000000..0d5e807 --- /dev/null +++ b/Docker/docker-compose.test.yml @@ -0,0 +1,12 @@ +name: arbeitszeitmessung-test +services: + db: + image: postgres:16 + restart: unless-stopped + env_file: + - .env.test + environment: + PGDATA: /var/lib/postgresql/data/pg_data + # volumes: //- ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d + ports: + - 5433:5432 diff --git a/Docker/docker-compose.yml b/Docker/docker-compose.yml index f484d08..272fc49 100644 --- a/Docker/docker-compose.yml +++ b/Docker/docker-compose.yml @@ -17,7 +17,7 @@ services: - 5432:5432 backend: - image: git.letsstein.de/tom/arbeitszeit-backend + image: git.letsstein.de/tom/arbeitszeitmessung env_file: - .env environment: diff --git a/Jenkinsfile b/Jenkinsfile index ee0171f..a5d5f5c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,24 +1,66 @@ pipeline { environment { - DOCKER_USERNAME = 'tom' - DOCKER_PASSWORD = credentials('dgitea_tom') - } + DOCKER_USERNAME = 'jenkins' + DOCKER_PASSWORD = credentials('gitea_jenkins') + SONAR_TOKEN = credentials('sonarcube_token') + POSTGRES_USER = 'postgres' + POSTGRES_PASSWORD = 'password' + POSTGRES_DB = 'arbeitszeitmessung' + } agent any stages { - stage ("Building image arbeitszeit-backend"){ - when { - anyOf{ - changeset 'Jenkinsfile' - changeset 'Makefile' - changeset 'Backend/**' + stage('Test') { + agent { + docker { + image '' + args '' + args '' } } steps { - sh 'make backend' - } + script { + sh ''' + docker run -d --rm \ + --name test-db \ + -e POSTGRES_USER={$POSTGRES_USER} \ + -e POSTGRES_PASSWORD={$POSTGRES_PASSWORD} \ + -e POSTGRES_DB={$POSTGRES_DB} \ + -v ./DB/initdb:/docker-entrypoint-initdb.d\ + -p "5432:5432" \ + postgres:16 + ''' + // docker.image('golang:1.24.5').withRun( + // '-u root:root --network=host' + // ) { go -> + // // wait for DB to start + // sh ''' + // cd Backend \ + // go mod download && go mod tidy \ + // go test ./... -v + // ''' + // } + } + } } + stage('SonarCube Analysis') { + steps { + sh 'make scan' + } + } + stage('Building image arbeitszeit-backend') { + when { + anyOf { + changeset 'Jenkinsfile' + changeset 'Makefile' + changeset 'Backend/**' + } + } + steps { + sh 'make backend' + } + } } } diff --git a/Makefile b/Makefile index 210097e..994d36c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ DOCKER_USERNAME ?= tom +PACKAGE_OWNER ?= tom DOCKER_PASSWORD ?= $(shell echo "YOUR_DEFAULT_PASSWORD") IMAGE_REGISTRY ?= git.letsstein.de APPLICATION_NAME ?= arbeitszeit @@ -16,12 +17,12 @@ ifdef JENKINS_HOME endif _builder: - docker build --tag ${IMAGE_REGISTRY}/${DOCKER_USERNAME}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} -f $(call capitalize, ${_BUILD_ARGS_APP_PART})/Dockerfile $(call capitalize, ${_BUILD_ARGS_APP_PART}) + docker build --tag ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} -f $(call capitalize, ${_BUILD_ARGS_APP_PART})/Dockerfile $(call capitalize, ${_BUILD_ARGS_APP_PART}) _pusher: login_registry - docker push ${IMAGE_REGISTRY}/${DOCKER_USERNAME}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} - docker tag ${IMAGE_REGISTRY}/${DOCKER_USERNAME}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} ${IMAGE_REGISTRY}/${DOCKER_USERNAME}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${_BUILD_ARGS_TAG} - docker push ${IMAGE_REGISTRY}/${DOCKER_USERNAME}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${_BUILD_ARGS_TAG} + docker push ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} + docker tag ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${_BUILD_ARGS_TAG} + docker push ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${_BUILD_ARGS_TAG} build_%: @@ -43,5 +44,11 @@ generateFrontend: backend: generateFrontend login_registry - docker buildx build --platform linux/amd64,linux/arm64 -t git.letsstein.de/tom/arbeitszeit-backend:latest Backend --push - docker buildx build --platform linux/amd64,linux/arm64 -t git.letsstein.de/tom/arbeitszeit-backend:${GIT_COMMIT} Backend --push + docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:latest Backend --push + docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:${GIT_COMMIT} Backend --push + +test: + $(MAKE) -C Backend test + +scan: test + $(MAKE) -C Backend scan diff --git a/db.sql b/db.sql index 0e59269..6f4d7fc 100644 --- a/db.sql +++ b/db.sql @@ -146,3 +146,121 @@ SELECT FROM ordered_bookings GROUP BY work_date ORDER BY work_date; + + +-- Generate weekdays for 2 weeks (Mon–Fri), starting 2 weeks ago +WITH days AS ( + SELECT gs::date AS work_date + FROM generate_series( + date_trunc('week', CURRENT_DATE) - interval '14 days', -- start 2 weeks ago Monday + CURRENT_DATE, -- end TODAY (no future days) + interval '1 day' + ) gs + WHERE EXTRACT(ISODOW FROM gs) <= 5 -- only Mon–Fri +), +sample_bookings AS ( + SELECT + d.work_date, + 'aaaa-aaaa'::varchar AS card_uid, + 1 AS check_in_out, -- come + 101 AS geraet_id, + (d.work_date + make_time(8, floor(random()*50)::int, 0))::timestamptz AS ts, + 1 AS anwesenheit_typ + FROM days d + UNION ALL + SELECT + d.work_date, + 'aaaa-aaaa'::varchar AS card_uid, + 2 AS check_in_out, -- go + 101 AS geraet_id, + (d.work_date + make_time(16, floor(random()*50)::int, 0))::timestamptz AS ts, + 1 AS anwesenheit_typ + FROM days d +), +ins_anw AS ( + -- insert only bookings up to now (prevents future times on today) + INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id) + SELECT ts, card_uid, check_in_out, geraet_id + FROM sample_bookings + WHERE ts <= NOW() + RETURNING 1 +) +-- now insert absences (uses the same days CTE) +INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum) +SELECT + 'aaaa-aaaa', + (ARRAY[1, 2])[floor(random()*2 + 1)], -- example types + d.work_date::timestamptz +FROM days d +WHERE random() < 0.2 -- ~20% random absences +ORDER BY d.work_date; + + +WITH params AS ( + SELECT + 'acde-edca'::varchar AS card_uid, + 101::int AS geraet_id, + 14::int AS start_days_ago, -- how many days back to start + 0::int AS end_days_ahead, -- how many days forward (0 = today) + 0.5::float AS pause_probability, + 0.2::float AS absence_probability +), +days AS ( + SELECT gs::date AS work_date, p.card_uid, p.geraet_id, p.pause_probability, p.absence_probability + FROM params p, + generate_series( + date_trunc('week', CURRENT_DATE) - (p.start_days_ago || ' days')::interval, + CURRENT_DATE + (p.end_days_ahead || ' days')::interval, + interval '1 day' + ) gs + WHERE EXTRACT(ISODOW FROM gs) <= 5 -- only Mon–Fri +), +base_bookings AS ( + -- come + SELECT + d.work_date, d.card_uid, 1 AS check_in_out, d.geraet_id, + (d.work_date + make_time(8, floor(random()*40)::int, 0))::timestamptz AS ts + FROM days d + UNION ALL + -- go + SELECT + d.work_date, d.card_uid, 2 AS check_in_out, d.geraet_id, + (d.work_date + make_time(16, floor(random()*40)::int, 0))::timestamptz AS ts + FROM days d +), +pause_bookings AS ( + -- pause come + SELECT + d.work_date, d.card_uid, 3 AS check_in_out, d.geraet_id, + (d.work_date + make_time(11, floor(random()*30)::int, 0))::timestamptz AS ts + FROM days d + WHERE random() < d.pause_probability + UNION ALL + -- pause go + SELECT + d.work_date, d.card_uid, 4 AS check_in_out, d.geraet_id, + (d.work_date + make_time(12, floor(random()*30)::int, 0))::timestamptz AS ts + FROM days d + WHERE random() < d.pause_probability +), +all_bookings AS ( + SELECT * FROM base_bookings + UNION ALL + SELECT * FROM pause_bookings +), +ins_anw AS ( + INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id) + SELECT ts, card_uid, check_in_out, geraet_id + FROM all_bookings + WHERE ts <= NOW() + ORDER BY work_date, ts + RETURNING 1 +) +INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum) +SELECT + d.card_uid, + (ARRAY[1, 2])[floor(random()*2 + 1)], -- example types + d.work_date::timestamptz +FROM days d +WHERE random() < d.absence_probability +ORDER BY d.work_date; diff --git a/migrations/20250901201159_initial.down.sql b/migrations/20250901201159_initial.down.sql new file mode 100644 index 0000000..4051a1f --- /dev/null +++ b/migrations/20250901201159_initial.down.sql @@ -0,0 +1,16 @@ +-- reverse: create "user_password" table +DROP TABLE "user_password"; +-- reverse: create "wochen_report" table +DROP TABLE "wochen_report"; +-- reverse: set comment to column: "geschlecht" on table: "personal_daten" +COMMENT ON COLUMN "personal_daten"."geschlecht" IS NULL; +-- reverse: create "personal_daten" table +DROP TABLE "personal_daten"; +-- reverse: set comment to column: "geraet_id" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."geraet_id" IS NULL; +-- reverse: set comment to column: "check_in_out" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."check_in_out" IS NULL; +-- reverse: create "anwesenheit" table +DROP TABLE "anwesenheit"; +-- reverse: create "abwesenheit" table +DROP TABLE "abwesenheit"; diff --git a/migrations/20250901201159_initial.up.sql b/migrations/20250901201159_initial.up.sql new file mode 100644 index 0000000..5144756 --- /dev/null +++ b/migrations/20250901201159_initial.up.sql @@ -0,0 +1,57 @@ +-- create "abwesenheit" table +CREATE TABLE "abwesenheit" ( + "counter_id" bigserial NOT NULL, + "card_uid" character varying(255) NULL, + "abwesenheit_typ" smallint NULL, + "datum" timestamptz NULL DEFAULT (now())::date, + PRIMARY KEY ("counter_id") +); +-- create "anwesenheit" table +CREATE TABLE "anwesenheit" ( + "counter_id" bigserial NOT NULL, + "timestamp" timestamptz NULL DEFAULT CURRENT_TIMESTAMP, + "card_uid" character varying(255) NULL, + "check_in_out" smallint NULL, + "geraet_id" smallint NULL, + PRIMARY KEY ("counter_id") +); +-- set comment to column: "check_in_out" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."check_in_out" IS '1=Check In 2=Check Out , 3=Check in Manuell, 4=Check out manuell255=Automatic Check Out'; +-- set comment to column: "geraet_id" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."geraet_id" IS 'ID des Lesegerätes'; +-- create "personal_daten" table +CREATE TABLE "personal_daten" ( + "personal_nummer" integer NOT NULL, + "aktiv_beschaeftigt" boolean NULL, + "vorname" character varying(255) NULL, + "nachname" character varying(255) NULL, + "geburtsdatum" date NULL, + "plz" character varying(255) NULL, + "adresse" character varying(255) NULL, + "geschlecht" smallint NULL, + "card_uid" character varying(255) NULL, + "hauptbeschaeftigungs_ort" smallint NULL, + "arbeitszeit_per_tag" real NULL, + "arbeitszeit_min_start" time NULL, + "arbeitszeit_max_ende" time NULL, + "vorgesetzter_pers_nr" integer NULL, + PRIMARY KEY ("personal_nummer") +); +-- set comment to column: "geschlecht" on table: "personal_daten" +COMMENT ON COLUMN "personal_daten"."geschlecht" IS '1==weiblich, 2==maennlich, 3==divers'; +-- create "user_password" table +CREATE TABLE "user_password" ( + "personal_nummer" integer NOT NULL, + "pass_hash" text NULL, + "zuletzt_geandert" timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("personal_nummer") +); +-- create "wochen_report" table +CREATE TABLE "wochen_report" ( + "id" serial NOT NULL, + "personal_nummer" integer NULL, + "woche_start" date NULL, + "bestaetigt" boolean NULL DEFAULT false, + PRIMARY KEY ("id"), + CONSTRAINT "wochen_report_personal_nummer_woche_start_key" UNIQUE ("personal_nummer", "woche_start") +); diff --git a/migrations/20250901201250_control_tables.down.sql b/migrations/20250901201250_control_tables.down.sql new file mode 100644 index 0000000..24ae700 --- /dev/null +++ b/migrations/20250901201250_control_tables.down.sql @@ -0,0 +1,14 @@ +-- reverse: remame "personal_daten" table +ALTER TABLE "s_personal_daten" RENAME TO "personal_daten"; + +-- reverse: create "s_anwesenheit_typen" table +DROP TABLE "s_anwesenheit_typen"; +-- reverse: create "s_abwesenheit_typen" table +DROP TABLE "s_abwesenheit_typen"; +-- reverse: modify "wochen_report" table +ALTER TABLE "wochen_report" DROP COLUMN "ueberstunden"; +-- reverse: modify "anwesenheit" table +ALTER TABLE "anwesenheit" DROP COLUMN "anwesenheit_typ", ALTER COLUMN "check_in_out" DROP NOT NULL, ALTER COLUMN "card_uid" DROP NOT NULL; + +-- reverse: rename a constraint from "personal_daten_pkey" to "s_personal_daten_pkey" +ALTER TABLE "s_personal_daten" RENAME CONSTRAINT "s_personal_daten_pkey" TO "personal_daten_pkey"; diff --git a/migrations/20250901201250_control_tables.up.sql b/migrations/20250901201250_control_tables.up.sql new file mode 100644 index 0000000..6e3eb2e --- /dev/null +++ b/migrations/20250901201250_control_tables.up.sql @@ -0,0 +1,21 @@ +-- modify "anwesenheit" table +ALTER TABLE "anwesenheit" ALTER COLUMN "card_uid" SET NOT NULL, ALTER COLUMN "check_in_out" SET NOT NULL, ADD COLUMN "anwesenheit_typ" smallint NULL; +-- modify "wochen_report" table +ALTER TABLE "wochen_report" ADD COLUMN "ueberstunden" smallint NULL; +-- create "s_abwesenheit_typen" table +CREATE TABLE "s_abwesenheit_typen" ( + "abwesenheit_id" smallint NOT NULL, + "abwesenheit_name" character varying(255) NULL, + PRIMARY KEY ("abwesenheit_id") +); +-- create "s_anwesenheit_typen" table +CREATE TABLE "s_anwesenheit_typen" ( + "anwesenheit_id" smallint NOT NULL, + "anwesenheit_name" character varying(255) NULL, + PRIMARY KEY ("anwesenheit_id") +); +-- rename "s_personal_daten" table +ALTER TABLE "personal_daten" RENAME TO "s_personal_daten"; + +-- rename a constraint from "personal_daten_pkey" to "s_personal_daten_pkey" +ALTER TABLE "s_personal_daten" RENAME CONSTRAINT "personal_daten_pkey" TO "s_personal_daten_pkey"; diff --git a/migrations/20250901201710_triggers_extension.down.sql b/migrations/20250901201710_triggers_extension.down.sql new file mode 100644 index 0000000..4260320 --- /dev/null +++ b/migrations/20250901201710_triggers_extension.down.sql @@ -0,0 +1,10 @@ +-- update Funktion für pass_hash + +DROP FUNCTION update_zuletzt_geandert; + +DROP TRIGGER IF EXISTS pass_hash_update ON user_password; + + +-- revert: Adds crypto extension + +DROP EXTENSION IF EXISTS pgcrypto; diff --git a/migrations/20250901201710_triggers_extension.up.sql b/migrations/20250901201710_triggers_extension.up.sql new file mode 100644 index 0000000..5e8ad1f --- /dev/null +++ b/migrations/20250901201710_triggers_extension.up.sql @@ -0,0 +1,21 @@ +-- update Funktion für pass_hash + +CREATE OR REPLACE FUNCTION update_zuletzt_geandert() +RETURNS TRIGGER AS $$ +BEGIN + -- Nur wenn hash geändert wurde + IF NEW.pass_hash IS DISTINCT FROM OLD.pass_hash THEN + NEW.zuletzt_geandert = now(); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER pass_hash_update +BEFORE UPDATE ON user_password +FOR EACH ROW +EXECUTE FUNCTION update_zuletzt_geandert(); + +-- Adds crypto extension + +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/migrations/20250903221313_overtime.down.sql b/migrations/20250903221313_overtime.down.sql new file mode 100644 index 0000000..956a8d5 --- /dev/null +++ b/migrations/20250903221313_overtime.down.sql @@ -0,0 +1,6 @@ +-- reverse: modify "wochen_report" table +ALTER TABLE "wochen_report" DROP COLUMN "arbeitszeit", ALTER COLUMN "ueberstunden" TYPE smallint; +-- reverse: modify "s_personal_daten" table +ALTER TABLE "s_personal_daten" DROP COLUMN "arbeitszeit_per_woche"; +-- reverse: modify "s_abwesenheit_typen" table +ALTER TABLE "s_abwesenheit_typen" DROP COLUMN "arbeitszeit_equivalent"; diff --git a/migrations/20250903221313_overtime.up.sql b/migrations/20250903221313_overtime.up.sql new file mode 100644 index 0000000..e2fb85c --- /dev/null +++ b/migrations/20250903221313_overtime.up.sql @@ -0,0 +1,6 @@ +-- modify "s_abwesenheit_typen" table +ALTER TABLE "s_abwesenheit_typen" ADD COLUMN "arbeitszeit_equivalent" real NULL; +-- modify "s_personal_daten" table +ALTER TABLE "s_personal_daten" ADD COLUMN "arbeitszeit_per_woche" real NULL; +-- modify "wochen_report" table +ALTER TABLE "wochen_report" ALTER COLUMN "ueberstunden" TYPE real, ADD COLUMN "arbeitszeit" real NULL; diff --git a/migrations/atlas.sum b/migrations/atlas.sum new file mode 100644 index 0000000..18be8b2 --- /dev/null +++ b/migrations/atlas.sum @@ -0,0 +1,9 @@ +h1:hyA7xJMv355hJfvtzD9tKSrkAEtsQ/vVixDZDDBxoh0= +20250901201159_initial.down.sql h1:cmF5CvNGqEfcmbRgiqaqDWERdNNRaMzarbNLJ/Y35o4= +20250901201159_initial.up.sql h1:Yrak/+wfQ4Tu/dVR/cUZ/75DlAcv4G/OJXDqpgSw47U= +20250901201250_control_tables.down.sql h1:f/KmhO9pOI45J8ZRjFonvD3CypB+rOoGOPN2WMFHvOw= +20250901201250_control_tables.up.sql h1:of5E07p0N1aen9CdQNEOrO7ffbKZC6kp4oK5KPzU9+g= +20250901201710_triggers_extension.down.sql h1:a9va3FSfHBWzODJSJO+ywNa2hiZwjG/vmvYGb3L1lnM= +20250901201710_triggers_extension.up.sql h1:nUBPd2eDssi/TwMVF/nOJkIM5rUM0iINdg1K9pZRZN0= +20250903221313_overtime.down.sql h1:X+jJESqcZ6ZTd2H563z6kRaXb4dn4sA02D3ck2795v8= +20250903221313_overtime.up.sql h1:C3DSiNVpe9v0Un1DEQ0lsy5yToR8iqcggv91GSr6tRE= diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7e4924a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1395 @@ +{ + "name": "arbeitszeitmessung", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@tailwindcss/cli": "^4.1.12", + "tailwindcss": "^4.1.12" + }, + "devDependencies": { + "@iconify-json/material-symbols-light": "^1.2.33", + "@iconify/tailwind4": "^1.0.6" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", + "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@iconify-json/material-symbols-light": { + "version": "1.2.33", + "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols-light/-/material-symbols-light-1.2.33.tgz", + "integrity": "sha512-OZNss65ipsMDkcX1a92MV7Bx6idC2UhztlnfdRUSSyfTnljfv4hn3H3SqwqgGbayvF2tDym79sq7lFAaQaCBEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/tailwind4": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@iconify/tailwind4/-/tailwind4-1.0.6.tgz", + "integrity": "sha512-43ZXe+bC7CuE2LCgROdqbQeFYJi/J7L/k1UpSy8KDQlWVsWxPzLSWbWhlJx4uRYLOh1NRyw02YlDOgzBOFNd+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/types": "^2.0.0", + "@iconify/utils": "^2.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/cyberalien" + }, + "peerDependencies": { + "tailwindcss": ">= 4" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@antfu/utils": "^8.1.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.0", + "globals": "^15.14.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "mlly": "^1.7.4" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.12.tgz", + "integrity": "sha512-2PyJ5MGh/6JPS+cEaAq6MGDx3UemkX/mJt+/phm7/VOpycpecwNnHuFZbbgx6TNK/aIjvFOhhTVlappM7tmqvQ==", + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", + "enhanced-resolve": "^5.18.3", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.1.12" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", + "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz", + "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-x64": "4.1.12", + "@tailwindcss/oxide-freebsd-x64": "4.1.12", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-x64-musl": "4.1.12", + "@tailwindcss/oxide-wasm32-wasi": "4.1.12", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz", + "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz", + "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz", + "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz", + "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz", + "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz", + "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz", + "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz", + "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz", + "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz", + "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", + "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz", + "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/magic-string": { + "version": "0.30.18", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", + "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/package-manager-detector": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", + "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fd0ca1d --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "@tailwindcss/cli": "^4.1.12", + "tailwindcss": "^4.1.12" + }, + "devDependencies": { + "@iconify-json/material-symbols-light": "^1.2.33", + "@iconify/tailwind4": "^1.0.6" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..18cb121 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,655 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/cli': + specifier: ^4.1.12 + version: 4.1.12 + tailwindcss: + specifier: ^4.1.12 + version: 4.1.12 + +packages: + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@tailwindcss/cli@4.1.12': + resolution: {integrity: sha512-2PyJ5MGh/6JPS+cEaAq6MGDx3UemkX/mJt+/phm7/VOpycpecwNnHuFZbbgx6TNK/aIjvFOhhTVlappM7tmqvQ==} + hasBin: true + + '@tailwindcss/node@4.1.12': + resolution: {integrity: sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==} + + '@tailwindcss/oxide-android-arm64@4.1.12': + resolution: {integrity: sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.12': + resolution: {integrity: sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.12': + resolution: {integrity: sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.12': + resolution: {integrity: sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': + resolution: {integrity: sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': + resolution: {integrity: sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.12': + resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.12': + resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.12': + resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.12': + resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': + resolution: {integrity: sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.12': + resolution: {integrity: sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.12': + resolution: {integrity: sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==} + engines: {node: '>= 10'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} + hasBin: true + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.1.12: + resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==} + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + +snapshots: + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.30': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + + '@tailwindcss/cli@4.1.12': + dependencies: + '@parcel/watcher': 2.5.1 + '@tailwindcss/node': 4.1.12 + '@tailwindcss/oxide': 4.1.12 + enhanced-resolve: 5.18.3 + mri: 1.2.0 + picocolors: 1.1.1 + tailwindcss: 4.1.12 + + '@tailwindcss/node@4.1.12': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.12 + + '@tailwindcss/oxide-android-arm64@4.1.12': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.12': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.12': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.12': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.12': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.12': + optional: true + + '@tailwindcss/oxide@4.1.12': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.12 + '@tailwindcss/oxide-darwin-arm64': 4.1.12 + '@tailwindcss/oxide-darwin-x64': 4.1.12 + '@tailwindcss/oxide-freebsd-x64': 4.1.12 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.12 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.12 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.12 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.12 + '@tailwindcss/oxide-linux-x64-musl': 4.1.12 + '@tailwindcss/oxide-wasm32-wasi': 4.1.12 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.12 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.12 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + chownr@3.0.0: {} + + detect-libc@1.0.3: {} + + detect-libc@2.0.4: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + graceful-fs@4.2.11: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@2.5.1: {} + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + + mri@1.2.0: {} + + node-addon-api@7.1.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + source-map-js@1.2.1: {} + + tailwindcss@4.1.12: {} + + tapable@2.2.2: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + yallist@5.0.0: {}