diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index c32282f..d2f0e60 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -2,56 +2,15 @@ name: Arbeitszeitmessung Deploy run-name: ${{ gitea.actor }} is building and deploying arbeitszeitmesssung on: push: - tags: "*" + tags: + - "*" + branches: + - main jobs: - testing: - name: Run Go Tests + webserver: + name: Build Webserver runs-on: ubuntu-latest - services: - postgres: - image: postgres:16 - env: - POSTGRES_USER: root - POSTGRES_PASSWORD: password - POSTGRES_DB: arbeitszeitmessung - env: - POSTGRES_HOST: postgres - POSTGRES_USER: root - POSTGRES_PASSWORD: password - POSTGRES_DB: arbeitszeitmessung - POSTGRES_PORT: 5432 - RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup go - uses: actions/setup-go@v5 - with: - go-version-file: Backend/go.mod - - uses: https://gitea.com/actions/go-hashfiles@v0.0.1 - id: hash-go - with: - patterns: | - go.mod - go.sum - - name: cache go - id: cache-go - uses: actions/cache@v4 - with: - path: |- - /go_path - /go_cache - key: arbeitszeitmessung-${{ steps.hash-go.outputs.hash }} - restore-keys: |- - arbeitszeitmessung- - - name: Run Go Tests - run: cd Backend && go test ./... - build: - name: Build Go Image and Upload - runs-on: ubuntu-latest - needs: [testing] steps: - name: Checkout uses: actions/checkout@v4 @@ -65,12 +24,49 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: git.letsstein.de/tom/arbeitszeitmessung-webserver + tags: | + type=raw,value=latest + type=pep440,pattern={{version}} - name: Build and push uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64 push: true context: Backend + tags: ${{ steps.meta.outputs.tags }} + document-creator: + name: Build Document Creator + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: git.letsstein.de + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: git.letsstein.de/tom/arbeitszeitmessung-doc-creator tags: | - git.letsstein.de/tom/arbeitszeitmessung:latest - git.letsstein.de/tom/arbeitszeitmessung:${{ github.ref_name }} + type=raw,value=latest + type=pep440,pattern={{version}} + - name: Build and push + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: true + context: Backend + tags: ${{ steps.meta.outputs.tags }} diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml index 2a3dfc4..80c1004 100644 --- a/.gitea/workflows/test.yaml +++ b/.gitea/workflows/test.yaml @@ -15,8 +15,8 @@ jobs: POSTGRES_DB: arbeitszeitmessung env: POSTGRES_HOST: postgres - POSTGRES_USER: root - POSTGRES_PASSWORD: password + POSTGRES_API_USER: root + POSTGRES_API_PASS: password POSTGRES_DB: arbeitszeitmessung POSTGRES_PORT: 5432 RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache diff --git a/.gitignore b/.gitignore index 13ae14c..cd09ad2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ node_modules atlas.hcl .scannerwork Backend/logs +.worktime.txt diff --git a/Backend/Makefile b/Backend/Makefile index 34598d0..c7f2e96 100644 --- a/Backend/Makefile +++ b/Backend/Makefile @@ -2,5 +2,16 @@ test: mkdir -p .test go test ./... -coverprofile=.test/coverage.out -json > .test/report.json -scan: - sonar-scanner -Dsonar.token=sqa_ca8394c93a728d6cff96703955288d8902c15200 +# scan: +# sonar-scanner -Dsonar.token=sqa_ca8394c93a728d6cff96703955288d8902c15200 + +# complete live run +live: + make -j2 live/templ live/tailwindcss + +live/templ: + templ generate --watch --proxy="http://localhost:8080" --cmd="go run ." --open-browser=false + +live/tailwindcss: + npx --yes tailwindcss -i ./src/main.css -o ./static/css/styles.css --watch +#--minify diff --git a/Backend/database.go b/Backend/database.go index bad1b77..8394d1f 100644 --- a/Backend/database.go +++ b/Backend/database.go @@ -14,7 +14,8 @@ func OpenDatabase() (models.IDatabase, error) { dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung") dbUser := helper.GetEnv("POSTGRES_API_USER", "api_nutzer") dbPassword := helper.GetEnv("POSTGRES_API_PASS", "password") + dbTz := helper.GetEnv("TZ", "Europe/Berlin") - connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=Europe/Berlin", dbUser, dbPassword, dbHost, dbName) + connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=%s", dbUser, dbPassword, dbHost, dbName, dbTz) return sql.Open("postgres", connStr) } diff --git a/Backend/endpoints/auto-feiertage.go b/Backend/endpoints/auto-feiertage.go new file mode 100644 index 0000000..6f32bca --- /dev/null +++ b/Backend/endpoints/auto-feiertage.go @@ -0,0 +1,43 @@ +package endpoints + +import ( + "arbeitszeitmessung/helper/paramParser" + "arbeitszeitmessung/models" + "log/slog" + "net/http" + "time" + + "github.com/wlbr/feiertage" +) + +func FeiertagsHandler(w http.ResponseWriter, r *http.Request) { + pp := paramParser.New(r.URL.Query()) + slog.Debug("Generating Holidays") + from := pp.ParseTimestampFallback("from", "2006", time.Now().AddDate(-1, 0, -time.Now().YearDay())) + to := pp.ParseTimestampFallback("to", "2006", time.Now().AddDate(0, 0, -time.Now().YearDay()+1)) + + var publicHolidays map[string]models.PublicHoliday = make(map[string]models.PublicHoliday) + + yearDelta := to.Year() - from.Year() + var holidays = feiertage.Sachsen(to.Year(), true) + for _, f := range holidays.Feiertage { + publicHolidays[f.Time.Format(time.DateOnly)] = models.NewHolidayFromFeiertag(f) + } + + repeatingHolidays, err := models.GetRepeatingHolidays(from, to.AddDate(0, 0, -1)) + if err != nil { + slog.Warn("Error getting holidays", slog.Any("Error", err)) + } + slog.Debug("Found repeating Holidays", "num", len(repeatingHolidays), "from", from, "to", to, "yeardelta", yearDelta) + for _, day := range repeatingHolidays { + day.Time = day.Time.AddDate(yearDelta, 0, 0) + publicHolidays[day.Date().Format(time.DateOnly)] = day + } + slog.Debug("Added repeating holidays", "num", len(holidays.Feiertage)) + for _, feiertag := range publicHolidays { + slog.Debug("Found holiday", "holiday", feiertag) + if err := feiertag.Insert(); err != nil { + slog.Warn("Error inserting Feiertag", slog.Any("Error", err)) + } + } +} diff --git a/Backend/endpoints/auto-kurzarbeit.go b/Backend/endpoints/auto-kurzarbeit.go new file mode 100644 index 0000000..10b2cd5 --- /dev/null +++ b/Backend/endpoints/auto-kurzarbeit.go @@ -0,0 +1,82 @@ +package endpoints + +import ( + "arbeitszeitmessung/helper" + "arbeitszeitmessung/helper/paramParser" + "arbeitszeitmessung/models" + "encoding/json" + "errors" + "log/slog" + "net/http" + "time" +) + +func KurzarbeitFillHandler(w http.ResponseWriter, r *http.Request) { + helper.SetCors(w) + switch r.Method { + case "GET": + fillKurzarbeit(r, w) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func fillKurzarbeit(r *http.Request, w http.ResponseWriter) { + bookingTypeKurzarbeit, err := getKurzarbeitBookingType() + if err != nil { + slog.Info("Error getting BookingType Kurzarbeit %v\n", slog.Any("Error", err)) + } + users, err := models.GetAllUsers() + if err != nil { + slog.Info("Error getting user list %v\n", slog.Any("Error", err)) + } + + pp := paramParser.New(r.URL.Query()) + startDate := pp.ParseTimestampFallback("date", time.DateOnly, time.Now()) + + var kurzarbeitAdded int + + for _, user := range users { + days := models.GetDays(user, startDate, startDate.AddDate(0, 0, 1), false) + if len(days) == 0 { + continue + } + + day := days[len(days)-1] + if !day.IsKurzArbeit() || !day.IsWorkDay() { + continue + } + if day.GetWorktime(user, models.WorktimeBaseDay, false) >= day.GetWorktime(user, models.WorktimeBaseDay, true) { + continue + } + + worktimeKurzarbeit := day.GetWorktime(user, models.WorktimeBaseDay, true) - day.GetWorktime(user, models.WorktimeBaseDay, false) + + if wDay, ok := day.(*models.WorkDay); !ok || len(wDay.Bookings) == 0 { + continue + } + workday, _ := day.(*models.WorkDay) + + lastBookingTime := workday.Bookings[len(workday.Bookings)-1].Timestamp + kurzarbeitBegin := (*models.Booking).New(nil, user.CardUID, 0, 1, bookingTypeKurzarbeit.Id) + kurzarbeitEnd := (*models.Booking).New(nil, user.CardUID, 0, 2, bookingTypeKurzarbeit.Id) + kurzarbeitBegin.Timestamp = lastBookingTime.Add(time.Minute) + kurzarbeitEnd.Timestamp = lastBookingTime.Add(worktimeKurzarbeit) + + kurzarbeitBegin.InsertWithTimestamp() + kurzarbeitEnd.InsertWithTimestamp() + kurzarbeitAdded += 1 + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(kurzarbeitAdded) +} + +func getKurzarbeitBookingType() (models.BookingType, error) { + for _, bookingType := range models.GetBookingTypesCached() { + if bookingType.Name == "Kurzarbeit" { + return bookingType, nil + } + } + return models.BookingType{}, errors.New("No Booking Type found") +} diff --git a/Backend/endpoints/auto_logout.go b/Backend/endpoints/auto-logout.go similarity index 81% rename from Backend/endpoints/auto_logout.go rename to Backend/endpoints/auto-logout.go index 7d6de92..e15f93a 100644 --- a/Backend/endpoints/auto_logout.go +++ b/Backend/endpoints/auto-logout.go @@ -20,8 +20,8 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { } func autoLogout(w http.ResponseWriter) { - users, err := (*models.User).GetAll(nil) - var logged_out_users []models.User + users, err := models.GetAllUsers() + var loggedOutUsers []models.User if err != nil { fmt.Printf("Error getting user list %v\n", err) } @@ -31,7 +31,7 @@ func autoLogout(w http.ResponseWriter) { if err != nil { fmt.Printf("Error logging out user %v\n", err) } else { - logged_out_users = append(logged_out_users, user) + loggedOutUsers = append(loggedOutUsers, user) log.Printf("Automaticaly logged out user %s, %s ", user.Name, user.Vorname) } } @@ -39,6 +39,6 @@ func autoLogout(w http.ResponseWriter) { } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(logged_out_users) + json.NewEncoder(w).Encode(loggedOutUsers) } diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go new file mode 100644 index 0000000..2557c0f --- /dev/null +++ b/Backend/endpoints/pdf-create.go @@ -0,0 +1,302 @@ +package endpoints + +import ( + "arbeitszeitmessung/helper" + "arbeitszeitmessung/helper/paramParser" + "arbeitszeitmessung/models" + "archive/zip" + "bytes" + "fmt" + "log" + "log/slog" + "net/http" + "time" + + "github.com/Dadido3/go-typst" +) + +const DE_DATE string = "02.01.2006" +const FILE_YEAR_MONTH string = "2006_01" + +func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) { + var typstDays []typstDay + for _, day := range days { + var thisTypstDay typstDay + work, pause, overtime := day.GetTimes(u, models.WorktimeBaseDay, false) + workVirtual := day.GetWorktime(u, models.WorktimeBaseDay, true) + overtime = workVirtual - u.ArbeitszeitProWocheFrac(0.2) + thisTypstDay.Date = day.Date().Format(DE_DATE) + thisTypstDay.Worktime = helper.FormatDurationFill(workVirtual, true) + thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) + thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true) + thisTypstDay.IsFriday = day.Date().Weekday() == time.Friday + + if workVirtual > work { + thisTypstDay.Kurzarbeit = helper.FormatDurationFill(workVirtual-work, true) + } else { + thisTypstDay.Kurzarbeit = helper.FormatDurationFill(0, true) + } + + thisTypstDay.DayParts = convertDayToTypstDayParts(day, u) + typstDays = append(typstDays, thisTypstDay) + } + return typstDays, nil +} + +func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDayPart { + var typstDayParts []typstDayPart + switch day.Type() { + case models.DayTypeWorkday: + workDay, _ := day.(*models.WorkDay) + for i := 0; i < len(workDay.Bookings); i += 2 { + var typstDayPart typstDayPart + typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04") + if i+1 < len(workDay.Bookings) { + typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04") + } else { + typstDayPart.BookingTo = workDay.Bookings[i].Timestamp.Format("15:04") + } + typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name + typstDayPart.IsWorkDay = true + typstDayParts = append(typstDayParts, typstDayPart) + } + if day.IsKurzArbeit() && len(workDay.Bookings) > 0 { + tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user) + typstDayParts = append(typstDayParts, typstDayPart{ + BookingFrom: tsFrom.Format("15:04"), + BookingTo: tsTo.Format("15:04"), + WorkType: "Kurzarbeit", + IsWorkDay: true, + }) + } + case models.DayTypeCompound: + for _, c := range day.(*models.CompoundDay).DayParts { + typstDayParts = append(typstDayParts, convertDayToTypstDayParts(c, user)...) + } + default: + typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: day.ToString()}) + } + return typstDayParts +} + +func PDFCreateController(w http.ResponseWriter, r *http.Request) { + helper.RequiresLogin(Session, w, r) + switch r.Method { + case http.MethodGet: + user, err := models.GetUserFromSession(Session, r.Context()) + if err != nil { + log.Println("Error getting user!") + return + } + pp := paramParser.New(r.URL.Query()) + startDate := pp.ParseTimestampFallback("start_date", time.DateOnly, time.Now()) + personalNumbers := pp.ParseIntListFallback("employe_list", ",", make([]int, 0)) + + employes, err := models.GetUserByPersonalNrMulti(personalNumbers) + if err != nil { + slog.Warn("Error getting employes!", slog.Any("Error", err)) + return + } + + n := 0 + for _, e := range employes { + if user.IsSuperior(e) { + employes[n] = e + n++ + } + } + employes = employes[:n] + + reportData := createReports(employes, startDate) + + switch pp.ParseStringFallback("output", "render") { + case "render": + output, err := renderPDFSingle(reportData) + if err != nil { + slog.Warn("Could not create pdf report", slog.Any("Error", err)) + w.WriteHeader(http.StatusInternalServerError) + } + w.Header().Set("Content-type", "application/pdf") + w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=Monatsabrechnung_%s", startDate.Format(FILE_YEAR_MONTH))) + output.WriteTo(w) + w.WriteHeader(http.StatusOK) + case "download": + pdfReports, err := renderPDFMulti(reportData) + if err != nil { + slog.Warn("Could not create pdf report", slog.Any("Error", err)) + w.WriteHeader(http.StatusInternalServerError) + } + output, err := zipPfd(pdfReports, &reportData) + if err != nil { + slog.Warn("Could not create pdf report", slog.Any("Error", err)) + w.WriteHeader(http.StatusInternalServerError) + } + w.Header().Set("Content-type", "application/zip") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachement; filename=Monatsabrechnung_%s", startDate.Format(FILE_YEAR_MONTH))) + output.WriteTo(w) + w.WriteHeader(http.StatusOK) + } + + default: + http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) + } +} + +func createReports(employes []models.User, startDate time.Time) []typstData { + startDate = helper.GetFirstOfMonth(startDate) + endDate := startDate.AddDate(0, 1, -1) + + var employeData []typstData + for _, employee := range employes { + if data, err := createEmployeReport(employee, startDate, endDate); err != nil { + slog.Warn("Error when creating employeReport", slog.Any("user", employee), slog.Any("error", err)) + } else { + employeData = append(employeData, data) + } + } + return employeData +} + +func createEmployeReport(employee models.User, startDate, endDate time.Time) (typstData, error) { + publicHolidays, err := models.GetHolidaysFromTo(startDate, endDate) + targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)-len(publicHolidays)) + workDaysThisMonth := models.GetDays(employee, startDate, endDate.AddDate(0, 0, 1), false) + + slog.Debug("Baseline Working hours", "targetHours", targetHoursThisMonth.Hours()) + + var workHours, kurzarbeitHours time.Duration + for _, day := range workDaysThisMonth { + tmpvirtualHours := day.GetWorktime(employee, models.WorktimeBaseDay, true) + tmpactualHours := day.GetWorktime(employee, models.WorktimeBaseDay, false) + if day.IsKurzArbeit() && tmpvirtualHours > tmpactualHours { + slog.Debug("Adding kurzarbeit to workday", "day", day.Date()) + kurzarbeitHours += tmpvirtualHours - tmpactualHours + } + workHours += tmpvirtualHours + } + worktimeBalance := workHours - targetHoursThisMonth + + typstDays, err := convertDaysToTypst(workDaysThisMonth, employee) + if err != nil { + slog.Warn("Failed to convert to days", slog.Any("error", err)) + return typstData{}, err + } + + metadata := typstMetadata{ + EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name), + TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)), + Overtime: helper.FormatDurationFill(worktimeBalance, true), + WorkTime: helper.FormatDurationFill(workHours, true), + Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true), + OvertimeTotal: "", + CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), + } + return typstData{Meta: metadata, Days: typstDays, FileName: fmt.Sprintf("%s_%s.pdf", startDate.Format(FILE_YEAR_MONTH), employee.Name)}, nil +} + +func renderPDFSingle(data []typstData) (bytes.Buffer, error) { + var markup bytes.Buffer + var output bytes.Buffer + + typstCLI := typst.DockerExec{ + ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"), + } + + if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil { + return output, err + } + + // Import the template and invoke the template function with the custom data. + // Show is used to replace the current document with whatever content the template function in `template.typ` returns. + markup.WriteString(` + #import "templates/abrechnung.typ": abrechnung + #for d in data { + abrechnung(d.Meta, d.Days) + } + `) + + // Compile the prepared markup with Typst and write the result it into `output.pdf`. + if err := typstCLI.Compile(&markup, &output, nil); err != nil { + return output, err + } + return output, nil +} + +func renderPDFMulti(data []typstData) ([]bytes.Buffer, error) { + var outputMulti []bytes.Buffer + + typstRender := typst.DockerExec{ + ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"), + } + + for _, d := range data { + var markup bytes.Buffer + var outputSingle bytes.Buffer + if err := typst.InjectValues(&markup, map[string]any{"meta": d.Meta, "days": d.Days}); err != nil { + return outputMulti, err + } + markup.WriteString(` + #import "templates/abrechnung.typ": abrechnung + #abrechnung(meta, days) + `) + + if err := typstRender.Compile(&markup, &outputSingle, nil); err != nil { + return outputMulti, err + } + outputMulti = append(outputMulti, outputSingle) + } + return outputMulti, nil +} + +func zipPfd(pdfReports []bytes.Buffer, reportData *[]typstData) (bytes.Buffer, error) { + var zipOutput bytes.Buffer + + zipWriter := zip.NewWriter(&zipOutput) + for index, report := range pdfReports { + zipFile, err := zipWriter.Create((*reportData)[index].FileName) + if err != nil { + fmt.Println(err) + } + _, err = zipFile.Write(report.Bytes()) + if err != nil { + fmt.Println(err) + } + } + + // Make sure to check the error on Close. + err := zipWriter.Close() + return zipOutput, err +} + +type typstMetadata struct { + TimeRange string `json:"time-range"` + EmployeeName string `json:"employee-name"` + WorkTime string `json:"worktime"` + Kurzarbeit string `json:"kurzarbeit"` + Overtime string `json:"overtime"` + OvertimeTotal string `json:"overtime-total"` + CurrentTimestamp string `json:"current-timestamp"` +} + +type typstDayPart struct { + BookingFrom string `json:"booking-from"` + BookingTo string `json:"booking-to"` + WorkType string `json:"worktype"` + IsWorkDay bool `json:"is-workday"` +} + +type typstDay struct { + Date string `json:"date"` + DayParts []typstDayPart `json:"day-parts"` + Worktime string `json:"worktime"` + Pausetime string `json:"pausetime"` + Overtime string `json:"overtime"` + Kurzarbeit string `json:"kurzarbeit"` + IsFriday bool `json:"is-weekend"` +} + +type typstData struct { + Meta typstMetadata `json:"meta"` + Days []typstDay `json:"days"` + FileName string +} diff --git a/Backend/endpoints/pdf.go b/Backend/endpoints/pdf.go index 87beb06..e08ef6d 100644 --- a/Backend/endpoints/pdf.go +++ b/Backend/endpoints/pdf.go @@ -4,38 +4,22 @@ import ( "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "arbeitszeitmessung/templates" - "log" + "log/slog" "net/http" - "time" ) -func PDFHandler(w http.ResponseWriter, r *http.Request) { +func PDFFormHandler(w http.ResponseWriter, r *http.Request) { helper.RequiresLogin(Session, w, r) - startDate, err := parseTimestamp(r, "start", time.Now().Format("2006-01-02")) - if err != nil { - log.Println("Error parsing 'start_date' time", err) - http.Error(w, "Timestamp 'start_date' cannot be parsed!", http.StatusBadRequest) - return - } - if startDate.Day() > 1 { - startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1)) - } - endDate := startDate.AddDate(0, 1, -1) user, err := models.GetUserFromSession(Session, r.Context()) if err != nil { - log.Println("Error getting user!") + slog.Warn("Error getting user!", slog.Any("Error", err)) + // TODO add error handling } - //TODO: only accepted weeks - - weeks := models.GetDays(user, startDate, endDate, false) - var aggregatedOvertime, aggregatedWorkTime time.Duration - for _, day := range weeks { - aggregatedOvertime += day.TimeOvertimeReal(user) - aggregatedWorkTime += day.TimeWorkVirtual(user) + teamMembers, err := user.GetTeamMembers() + if err != nil { + slog.Warn("Error getting team members!", slog.Any("Error", err)) } - - // log.Printf("Using Dates: %s - %s\n", startDate.String(), endDate.String()) - templates.PDFReportEmploye(user, aggregatedOvertime, aggregatedWorkTime, weeks, startDate, endDate).Render(r.Context(), w) + templates.PDFForm(teamMembers).Render(r.Context(), w) } diff --git a/Backend/endpoints/team_presence.go b/Backend/endpoints/team-presence.go similarity index 100% rename from Backend/endpoints/team_presence.go rename to Backend/endpoints/team-presence.go diff --git a/Backend/endpoints/team.go b/Backend/endpoints/team.go index 397d45a..efc1c7b 100644 --- a/Backend/endpoints/team.go +++ b/Backend/endpoints/team.go @@ -2,13 +2,13 @@ package endpoints import ( "arbeitszeitmessung/helper" + "arbeitszeitmessung/helper/paramParser" "arbeitszeitmessung/models" "arbeitszeitmessung/templates" "context" "errors" "log" "net/http" - "strconv" "time" ) @@ -30,17 +30,17 @@ func submitReport(w http.ResponseWriter, r *http.Request) { log.Println("Error parsing form", err) return } - userPN, _ := strconv.Atoi(r.FormValue("user")) - _weekTs := r.FormValue("week") - weekTs, err := time.Parse(time.DateOnly, _weekTs) + pp := paramParser.New(r.Form) + userPN, err := pp.ParseInt("user") + weekTs := pp.ParseTimestampFallback("week", time.DateOnly, time.Now()) user, err := models.GetUserByPersonalNr(userPN) - workWeek := models.NewWorkWeek(user, weekTs, true) - if err != nil { log.Println("Could not get user!") return } + workWeek := models.NewWorkWeek(user, weekTs, true) + switch r.FormValue("method") { case "send": err = workWeek.SendWeek() @@ -62,14 +62,11 @@ func showWeeks(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/user/login", http.StatusSeeOther) return } - submissionDate := r.URL.Query().Get("submission_date") - lastSub := user.GetLastWorkWeekSubmission() - if submissionDate != "" { - submissionDate, err := time.Parse("2006-01-02", submissionDate) - if err == nil { - lastSub = helper.GetMonday(submissionDate) - } - } + + pp := paramParser.New(r.URL.Query()) + submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission()) + lastSub := helper.GetMonday(submissionDate) + userWeek := models.NewWorkWeek(user, lastSub, true) var workWeeks []models.WorkWeek diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index 102c3f2..1f7985e 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -2,14 +2,15 @@ package endpoints import ( "arbeitszeitmessung/helper" + "arbeitszeitmessung/helper/paramParser" "arbeitszeitmessung/models" "arbeitszeitmessung/templates" "context" "database/sql" "encoding/json" "log" + "log/slog" "net/http" - "sort" "strconv" "time" ) @@ -36,8 +37,18 @@ func AbsencHandler(w http.ResponseWriter, r *http.Request) { helper.SetCors(w) switch r.Method { case http.MethodPost: - err := updateAbsence(r) + r.ParseForm() + var err error + switch r.FormValue("action") { + case "insert": + err = updateAbsence(r) + case "delete": + err = deleteAbsence(r) + default: + slog.Warn("No action found!") + } if err != nil { + slog.Warn("Error handling absence route ", "error", err) http.Error(w, "Internal error", http.StatusInternalServerError) return } @@ -52,7 +63,7 @@ func parseTimestamp(r *http.Request, getKey string, fallback string) (time.Time, if getTimestamp == "" { getTimestamp = fallback } - Timestamp, err := time.Parse("2006-01-02", getTimestamp) + Timestamp, err := time.Parse(time.DateOnly, getTimestamp) if err != nil { return time.Now(), err } @@ -67,26 +78,15 @@ func getBookings(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/user/login", http.StatusSeeOther) return } + pp := paramParser.New(r.URL.Query()) // TODO add config for timeoffset - tsFrom, err := parseTimestamp(r, "time_from", time.Now().AddDate(0, -1, 0).Format("2006-01-02")) - if err != nil { - log.Println("Error parsing 'from' time", err) - http.Error(w, "Timestamp 'from' cannot be parsed!", http.StatusBadRequest) - return - } - tsTo, err := parseTimestamp(r, "time_to", time.Now().Format("2006-01-02")) - if err != nil { - log.Println("Error parsing 'to' time", err) - http.Error(w, "Timestamp 'to' cannot be parsed!", http.StatusBadRequest) - return - } + tsFrom := pp.ParseTimestampFallback("time_from", time.DateOnly, time.Now().AddDate(0, -1, 0)) + tsTo := pp.ParseTimestampFallback("time_to", time.DateOnly, time.Now()) + tsTo = tsTo.AddDate(0, 0, 1) // so that today is inside days := models.GetDays(user, tsFrom, tsTo, true) - sort.Slice(days, func(i, j int) bool { - return days[i].Date().After(days[j].Date()) - }) lastSub := user.GetLastWorkWeekSubmission() var aggregatedOvertime time.Duration @@ -94,7 +94,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) { if day.Date().Before(lastSub) { continue } - aggregatedOvertime += day.TimeOvertimeReal(user) + aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, false) } if reportedOvertime, err := user.GetReportedOvertime(); err == nil { user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute) @@ -114,8 +114,24 @@ func getBookings(w http.ResponseWriter, r *http.Request) { templates.TimePage([]models.WorkDay{}, lastSub).Render(ctx, w) } +func deleteAbsence(r *http.Request) error { + r.ParseForm() + pp := paramParser.New(r.Form) + counterId, err := pp.ParseInt("aw_id") + + if err != nil { + return err + } + absence, err := models.GetAbsenceById(counterId) + if err != nil { + return err + } + return absence.Delete() +} + func updateBooking(w http.ResponseWriter, r *http.Request) { r.ParseForm() + pp := paramParser.New(r.Form) var loc *time.Location loc, err := time.LoadLocation(helper.GetEnv("TZ", "Europe/Berlin")) if err != nil { @@ -136,10 +152,9 @@ func updateBooking(w http.ResponseWriter, r *http.Request) { return } - var check_in_out int - check_in_out, err = strconv.Atoi(r.FormValue("check_in_out")) + check_in_out, err := pp.ParseInt("check_in_out") if err != nil { - log.Println("Error parsing check_in_out", err) + slog.Warn("Error parsing check_in_out") return } @@ -193,13 +208,13 @@ func updateAbsence(r *http.Request) error { loc = time.Local } - dateFrom, err := time.ParseInLocation("2006-01-02", r.FormValue("date_from"), loc) + dateFrom, err := time.ParseInLocation(time.DateOnly, r.FormValue("date_from"), loc) if err != nil { log.Println("Error parsing date_from input for absence", err) return err } - dateTo, err := time.ParseInLocation("2006-01-02", r.FormValue("date_to"), loc) + dateTo, err := time.ParseInLocation(time.DateOnly, r.FormValue("date_to"), loc) if err != nil { log.Println("Error parsing date_to input for absence", err) return err @@ -261,22 +276,3 @@ func updateAbsence(r *http.Request) error { return nil } - -func createAbsence(absenceType int, user models.User, loc *time.Location, r *http.Request) { - absenceDate, err := time.ParseInLocation("2006-01-02", r.FormValue("date"), loc) - if err != nil { - log.Println("Cannot get date from input! Skipping absence creation", err) - return - } - - absence, err := models.NewAbsence(user.CardUID, absenceType, absenceDate) - if err != nil { - log.Println("Error creating absence!", err) - return - } - err = absence.Insert() - if err != nil { - log.Println("Error inserting absence!", err) - return - } -} diff --git a/Backend/endpoints/user-settings.go b/Backend/endpoints/user-settings.go index 66f7d1b..65b7ebd 100644 --- a/Backend/endpoints/user-settings.go +++ b/Backend/endpoints/user-settings.go @@ -43,5 +43,5 @@ func showUserPage(w http.ResponseWriter, r *http.Request, status int) { if user, err := models.GetUserFromSession(Session, r.Context()); err == nil { ctx = context.WithValue(r.Context(), "user", user) } - templates.UserPage(status).Render(ctx, w) + templates.SettingsPage(status).Render(ctx, w) } diff --git a/Backend/go.mod b/Backend/go.mod index b993a01..3c70ca4 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -4,11 +4,14 @@ go 1.24.7 require github.com/lib/pq v1.10.9 -require github.com/a-h/templ v0.3.943 +require github.com/a-h/templ v0.3.960 require github.com/alexedwards/scs/v2 v2.8.0 +require github.com/wlbr/feiertage v1.17.0 + require ( + github.com/Dadido3/go-typst v0.8.0 github.com/golang-migrate/migrate/v4 v4.18.3 github.com/joho/godotenv v1.5.1 ) @@ -16,6 +19,14 @@ require ( require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/smasher164/xid v0.1.2 // indirect go.uber.org/atomic v1.7.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/tools v0.38.0 // indirect ) + +tool golang.org/x/tools/cmd/deadcode diff --git a/Backend/go.sum b/Backend/go.sum index 6c7522d..0c9f849 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -1,11 +1,19 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Dadido3/go-typst v0.8.0 h1:uTLYprhkrBjwsCXRRuyYUFL0fpYHa2kIYoOB/CGqVNs= +github.com/Dadido3/go-typst v0.8.0/go.mod h1:QYis9sT70u65kn1SkFfyPRmHsPxgoxWbAixwfPReOZA= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY= github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= +github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM= +github.com/a-h/templ v0.3.960/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/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 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= @@ -38,6 +46,7 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l 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/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 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= @@ -54,10 +63,25 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/smasher164/xid v0.1.2 h1:erplXSdBRIIw+MrwjJ/m8sLN2XY16UGzpTA0E2Ru6HA= +github.com/smasher164/xid v0.1.2/go.mod h1:tgivm8CQl19fH1c5y+8F4mA+qY6n2i6qDRBlY/6nm+I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/wlbr/feiertage v1.10.0/go.mod h1:wJOHvMa6sI5L1FkrTOX/GSoO0hpK3S2YqGLPi8Q84I0= +github.com/wlbr/feiertage v1.17.0 h1:AEck/iUQu19iU0xNEoSQTeSTGXF1Ju0tbAwEi/Lmwqk= +github.com/wlbr/feiertage v1.17.0/go.mod h1:TVZgmSZgGW/jSxexZ56qdlR6cDj+F/FO8bkw8U6kYxM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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= @@ -68,7 +92,65 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3 go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/Backend/helper/logs/main.go b/Backend/helper/logs/main.go index 764a51c..9cad2bb 100644 --- a/Backend/helper/logs/main.go +++ b/Backend/helper/logs/main.go @@ -14,7 +14,7 @@ type FileLog struct { var Logs map[string]FileLog = make(map[string]FileLog) func NewAudit() (i *log.Logger, close func() error) { - LOG_FILE := "logs/" + time.Now().Format("2006-01-02") + ".log" + LOG_FILE := "logs/" + time.Now().Format(time.DateOnly) + ".log" logFile, err := os.OpenFile(LOG_FILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Panic(err) diff --git a/Backend/helper/paramParser/main.go b/Backend/helper/paramParser/main.go new file mode 100644 index 0000000..8dd6586 --- /dev/null +++ b/Backend/helper/paramParser/main.go @@ -0,0 +1,117 @@ +package paramParser + +import ( + "fmt" + "log/slog" + "net/url" + "strconv" + "strings" + "time" +) + +type ParamsParser struct { + urlParams url.Values +} + +func (p ParamsParser) ParseStringListFallback(key string, delimiter string, fallback []string) []string { + if !p.urlParams.Has(key) { + return fallback + } + paramList := p.urlParams.Get(key) + list := strings.Split(paramList, delimiter) + return list +} + +func (p ParamsParser) ParseIntListFallback(key string, delimiter string, fallback []int) []int { + if !p.urlParams.Has(key) { + return fallback + } + paramList := p.urlParams[key] + parsedList := make([]int, 0) + for _, item := range paramList { + if parsedItem, err := strconv.Atoi(item); err == nil { + parsedList = append(parsedList, parsedItem) + } + } + return parsedList +} + +type NoValueError struct { + Key string +} + +func (e *NoValueError) Error() string { + return fmt.Sprintf("No value found for key %s", e.Key) +} + +func New(params url.Values) ParamsParser { + return ParamsParser{ + urlParams: params, + } +} + +func (p *ParamsParser) ParseTimestampFallback(key string, format string, fallback time.Time) time.Time { + if !p.urlParams.Has(key) { + return fallback + } + paramTimestamp := p.urlParams.Get(key) + if timestamp, err := time.Parse(format, paramTimestamp); err == nil { + return timestamp + } else { + slog.Warn("Error parsing HTTP Params to time.Time", slog.Any("key", key), slog.Any("error", err)) + return fallback + } +} + +func (p *ParamsParser) ParseTimestamp(key string, format string) (time.Time, error) { + if !p.urlParams.Has(key) { + return time.Time{}, &NoValueError{Key: key} + } + paramTimestamp := p.urlParams.Get(key) + if timestamp, err := time.Parse(format, paramTimestamp); err == nil { + return timestamp, nil + } else { + slog.Debug("Error parsing HTTP Params to time.Time", slog.Any("key", key), slog.Any("error", err)) + return timestamp, err + } +} + +func (p *ParamsParser) ParseStringFallback(key string, fallback string) string { + if !p.urlParams.Has(key) { + return fallback + } + return p.urlParams.Get(key) +} + +func (p *ParamsParser) ParseString(key string) (string, error) { + if !p.urlParams.Has(key) { + return "", &NoValueError{Key: key} + } + return p.urlParams.Get(key), nil +} + +func (p *ParamsParser) ParseIntFallback(key string, fallback int) int { + if !p.urlParams.Has(key) { + return fallback + } + paramInt := p.urlParams.Get(key) + if result, err := strconv.Atoi(paramInt); err == nil { + return result + } else { + slog.Warn("Error parsing HTTP Params to Int", slog.Any("key", key), slog.Any("error", err)) + return fallback + } +} + +func (p *ParamsParser) ParseInt(key string) (int, error) { + if !p.urlParams.Has(key) { + return 0, &NoValueError{Key: key} + } + paramInt := p.urlParams.Get(key) + if result, err := strconv.Atoi(paramInt); err == nil { + return result, nil + } else { + slog.Debug("Error parsing HTTP Params to Int", slog.Any("key", key), slog.Any("error", err)) + return 0, err + } +} diff --git a/Backend/helper/strings_test.go b/Backend/helper/strings_test.go new file mode 100644 index 0000000..ac1b4f3 --- /dev/null +++ b/Backend/helper/strings_test.go @@ -0,0 +1,47 @@ +package helper + +import "testing" + +func TestGetFirst(t *testing.T) { + tests := []struct { + name string + a any + b any + want any + }{ + {"ints", 10, 20, 10}, + {"strings", "first", "second", "first"}, + {"mixed", "abc", 123, "abc"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetFirst(tt.a, tt.b) + if got != tt.want { + t.Errorf("GetFirst(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want) + } + }) + } +} + +func TestGetSecond(t *testing.T) { + tests := []struct { + name string + a any + b any + want any + }{ + {"ints", 10, 20, 20}, + {"strings", "first", "second", "second"}, + {"mixed", "abc", 123, 123}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetSecond(tt.a, tt.b) + if got != tt.want { + t.Errorf("GetSecond(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want) + } + }) + } +} diff --git a/Backend/helper/time.go b/Backend/helper/time.go index 046ad64..1337485 100644 --- a/Backend/helper/time.go +++ b/Backend/helper/time.go @@ -16,6 +16,13 @@ func GetMonday(ts time.Time) time.Time { return ts } +func GetFirstOfMonth(ts time.Time) time.Time { + if ts.Day() > 1 { + return ts.AddDate(0, 0, -(ts.Day() - 1)) + } + return ts +} + func IsWeekend(ts time.Time) bool { return ts.Weekday() == time.Saturday || ts.Weekday() == time.Sunday } @@ -55,3 +62,23 @@ func FormatDurationFill(d time.Duration, fill bool) string { func IsSameDate(a, b time.Time) bool { return a.Truncate(24 * time.Hour).Equal(b.Truncate(24 * time.Hour)) } + +func GetWorkingDays(startDate, endDate time.Time) int { + if endDate.Before(startDate) { + return 0 + } + var count int = 0 + for d := startDate.Truncate(24 * time.Hour); !d.After(endDate); d = d.Add(24 * time.Hour) { + if !IsWeekend(d) { + count++ + } + } + return count +} + +func FormatGermanDayOfWeek(t time.Time) string { + return days[t.Weekday()][:2] +} + +var days = [...]string{ + "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"} diff --git a/Backend/helper/time_test.go b/Backend/helper/time_test.go index 454d4e3..4e6e67e 100644 --- a/Backend/helper/time_test.go +++ b/Backend/helper/time_test.go @@ -1,37 +1,172 @@ package helper import ( + "fmt" "testing" "time" ) func TestGetMonday(t *testing.T) { - isMonday, err := time.Parse("2006-01-02", "2025-07-14") - notMonday, err := time.Parse("2006-01-02", "2025-07-16") + isMonday, err := time.Parse(time.DateOnly, "2025-07-14") + isSunday, err := time.Parse(time.DateOnly, "2025-07-20") + notMonday, err := time.Parse(time.DateOnly, "2025-07-16") if err != nil || isMonday.Equal(notMonday) { t.Errorf("U stupid? %e", err) } - if GetMonday(isMonday) != isMonday || GetMonday(notMonday) != isMonday { + if GetMonday(isMonday) != isMonday { t.Error("Wrong date conversion!") } + + if GetMonday(notMonday) != isMonday { + t.Error("Wrong date conversion (notMonday)!") + } + + if GetMonday(isSunday) != isMonday { + t.Error("Wrong date conversion (isSunday)!") + } } -func TestFormatDuration(t *testing.T) { - durations := []struct { +func TestFormatDurationFill(t *testing.T) { + testCases := []struct { name string duration time.Duration + fill bool }{ - {"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}, + {"2h", time.Duration(120 * time.Minute), true}, + {"30min", time.Duration(30 * time.Minute), true}, + {"1h 30min", time.Duration(90 * time.Minute), true}, + {"-1h 30min", time.Duration(-90 * time.Minute), true}, + {"0min", 0, true}, + {"", 0, false}, } - for _, d := range durations { - t.Run(d.name, func(t *testing.T) { - if FormatDuration(d.duration) != d.name { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if FormatDurationFill(tc.duration, tc.fill) != tc.name { t.Error("Format missmatch in Formatduration.") } }) } } + +func TestFormatDuration(t *testing.T) { + testCases := []struct { + name string + duration time.Duration + }{ + {"", 0}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if FormatDuration(tc.duration) != tc.name { + t.Error("Format missmatch in Formatduration.") + } + }) + } +} + +func TestIsSameDate(t *testing.T) { + testCases := []struct { + dateA string + dateB string + result bool + }{ + {"2025-12-01 00:00:00", "2025-12-01 00:00:00", true}, + {"2025-12-03 00:00:00", "2025-12-02 00:00:00", false}, + {"2025-12-03 23:45:00", "2025-12-03 00:00:00", true}, + {"2025-12-04 24:12:00", "2025-12-04 00:12:00", false}, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("IsSameDateTest: %s date", tc.dateA), func(t *testing.T) { + dateA, _ := time.Parse(time.DateTime, tc.dateA) + dateB, _ := time.Parse(time.DateTime, tc.dateB) + if IsSameDate(dateA, dateB) != tc.result { + t.Errorf("Is SameDate did not match! Result %t", IsSameDate(dateA, dateB)) + } + }) + } +} + +func TestGetWorkingDays(t *testing.T) { + testCases := []struct { + start string + end string + days int + }{ + {"2025-10-01", "2025-10-02", 2}, + {"2025-10-02", "2025-10-01", 0}, + {"2025-10-01", "2025-10-31", 23}, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("WorkingDayTest: %d days", tc.days), func(t *testing.T) { + startDate, _ := time.Parse(time.DateOnly, tc.start) + endDate, _ := time.Parse(time.DateOnly, tc.end) + if GetWorkingDays(startDate, endDate) != tc.days { + t.Error("Calculated workdays do not match target") + } + }) + } +} + +func TestFormatGermanDayOfWeek(t *testing.T) { + testCases := []struct { + date string + result string + }{ + {"2025-12-01", "Mo"}, + {"2025-12-02", "Di"}, + {"2025-12-03", "Mi"}, + {"2025-12-04", "Do"}, + {"2025-12-05", "Fr"}, + {"2025-12-06", "Sa"}, + {"2025-12-07", "So"}, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("FormatWeekDayTest: %s date", tc.date), func(t *testing.T) { + date, _ := time.Parse(time.DateOnly, tc.date) + if FormatGermanDayOfWeek(date) != tc.result { + t.Error("Formatted workday did not match!") + } + }) + } +} + +func TestGetKW(t *testing.T) { + tests := []struct { + name string + date time.Time + want int + }{ + { + name: "First week of year", + date: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC), // Monday + want: 1, + }, + { + name: "Middle of year", + date: time.Date(2023, 6, 15, 0, 0, 0, 0, time.UTC), + want: 24, + }, + { + name: "Last week of year", + date: time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC), + want: 52, + }, + { + name: "ISO week crossing into next year", + date: time.Date(2020, 12, 31, 0, 0, 0, 0, time.UTC), + want: 53, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetKW(tt.date) + if got != tt.want { + t.Errorf("GetKW(%v) = %d, want %d", tt.date, got, tt.want) + } + }) + } +} diff --git a/Backend/helper/types_test.go b/Backend/helper/types_test.go new file mode 100644 index 0000000..deb9821 --- /dev/null +++ b/Backend/helper/types_test.go @@ -0,0 +1,26 @@ +package helper + +import ( + "fmt" + "testing" +) + +func TestBoolToInt(t *testing.T) { + testCases := []struct { + value bool + res int + res8 int8 + }{ + {true, 1, 1}, + {false, 0, 0}, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("BoolToInt value: %t", tc.value), func(t *testing.T) { + if BoolToInt(tc.value) != tc.res || BoolToInt8(tc.value) != tc.res8 { + t.Error("How could you... mess up bool to int") + } + }) + } + +} diff --git a/Backend/helper/web_test.go b/Backend/helper/web_test.go new file mode 100644 index 0000000..5d39886 --- /dev/null +++ b/Backend/helper/web_test.go @@ -0,0 +1,112 @@ +package helper + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/alexedwards/scs/v2" +) + +func TestSetCors_WhenNoCorsTrue(t *testing.T) { + os.Setenv("NO_CORS", "true") + defer os.Unsetenv("NO_CORS") + + rr := httptest.NewRecorder() + + SetCors(rr) + + h := rr.Header() + + if h.Get("Access-Control-Allow-Origin") != "*" { + t.Errorf("expected Access-Control-Allow-Origin to be '*', got %q", h.Get("Access-Control-Allow-Origin")) + } + + if h.Get("Access-Control-Allow-Methods") != "*" { + t.Errorf("expected Access-Control-Allow-Methods to be '*', got %q", h.Get("Access-Control-Allow-Methods")) + } + + if h.Get("Access-Control-Allow-Headers") != "*" { + t.Errorf("expected Access-Control-Allow-Headers to be '*', got %q", h.Get("Access-Control-Allow-Headers")) + } +} + +func TestSetCors_WhenNoCorsFalse(t *testing.T) { + os.Setenv("NO_CORS", "false") + defer os.Unsetenv("NO_CORS") + + rr := httptest.NewRecorder() + + SetCors(rr) + + h := rr.Header() + if h.Get("Access-Control-Allow-Origin") != "" || + h.Get("Access-Control-Allow-Methods") != "" || + h.Get("Access-Control-Allow-Headers") != "" { + t.Errorf("CORS headers should not be set when NO_CORS=false") + } +} + +func TestRequiresLogin_DebugMode_NoRedirect(t *testing.T) { + os.Setenv("GO_ENV", "debug") + defer os.Unsetenv("GO_ENV") + + session := scs.New() + + req := httptest.NewRequest("GET", "/", nil) + rr := httptest.NewRecorder() + + RequiresLogin(session, rr, req) + + if rr.Result().StatusCode == http.StatusSeeOther { + t.Errorf("expected no redirect in debug mode") + } +} + +// func TestRequiresLogin_UserExists_NoRedirect(t *testing.T) { +// os.Setenv("GO_ENV", "production") +// defer os.Unsetenv("GO_ENV") + +// session := scs.New() + +// req := httptest.NewRequest("GET", "/", nil) +// ctx, err := session.Load(req.Context(), "") +// if err != nil { +// t.Fatalf("session load error: %v", err) +// } + +// ctx = session.Put(ctx, "user", "123") +// req = req.WithContext(context.WithValue(ctx, "session", session)) + +// rr := httptest.NewRecorder() + +// yourpkg.RequiresLogin(session, rr, req) + +// if rr.Result().StatusCode == http.StatusSeeOther { +// t.Errorf("expected no redirect when user exists") +// } +// } + +// func TestRequiresLogin_NoUser_Redirects(t *testing.T) { +// os.Setenv("GO_ENV", "production") +// defer os.Unsetenv("GO_ENV") + +// session := scs.New() + +// req := httptest.NewRequest("GET", "/", nil) +// req = req.WithContext(context.WithValue(req.Context(), "session", session)) + +// rr := httptest.NewRecorder() + +// RequiresLogin(session, rr, req) + +// if rr.Result().StatusCode != http.StatusSeeOther { +// t.Errorf("expected redirect when user does not exist, got %d", rr.Result().StatusCode) +// } + +// location := rr.Result().Header.Get("Location") +// if location != "/user/login" { +// t.Errorf("expected redirect to /user/login, got %q", location) +// } +// } diff --git a/Backend/main.go b/Backend/main.go index 35395e2..4832dc0 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -5,8 +5,7 @@ import ( "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "context" - "fmt" - "log" + "log/slog" "net/http" "os" "time" @@ -17,23 +16,25 @@ import ( func main() { var err error + var logLevel slog.LevelVar + logLevel.Set(slog.LevelWarn) + + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &logLevel})) + slog.SetDefault(logger) err = godotenv.Load(".env") if err != nil { - log.Println("No .env file found in directory!") + slog.Info("No .env file found in directory!") } if helper.GetEnv("GO_ENV", "production") == "debug" { - log.Println("Debug mode enabled") - log.Println("Environment Variables") + logLevel.Set(slog.LevelDebug) envs := os.Environ() - for _, e := range envs { - fmt.Println(e) - } + slog.Debug("Debug mode enabled", "Environment Variables", envs) } models.DB, err = OpenDatabase() if err != nil { - log.Fatal(err) + slog.Error("Error while opening the database", "Error", err) } fs := http.FileServer(http.Dir("./static")) @@ -45,27 +46,31 @@ func main() { server.HandleFunc("/time/new", endpoints.TimeCreateHandler) server.Handle("/absence", ParamsMiddleware(endpoints.AbsencHandler)) server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler)) - server.HandleFunc("/logout", endpoints.LogoutHandler) + server.HandleFunc("/auto/logout", endpoints.LogoutHandler) + server.HandleFunc("/auto/kurzarbeit", endpoints.KurzarbeitFillHandler) + server.HandleFunc("/auto/feiertage", endpoints.FeiertagsHandler) server.HandleFunc("/user/{action}", endpoints.UserHandler) // server.HandleFunc("/user/login", endpoints.LoginHandler) // server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) server.HandleFunc("/team", endpoints.TeamHandler) - server.HandleFunc("/team/presence", endpoints.TeamPresenceHandler) - server.HandleFunc("/pdf", endpoints.PDFHandler) + server.HandleFunc("/presence", endpoints.TeamPresenceHandler) + server.Handle("/pdf", ParamsMiddleware(endpoints.PDFFormHandler)) + server.HandleFunc("/pdf/generate", endpoints.PDFCreateController) server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect)) server.Handle("/static/", http.StripPrefix("/static/", fs)) serverSessionMiddleware := endpoints.Session.LoadAndSave(server) // starting the http server - fmt.Printf("Server is running at http://localhost:%s\n", helper.GetEnv("EXPOSED_PORT", "8080")) - log.Fatal(http.ListenAndServe(":8080", serverSessionMiddleware)) + slog.Info("Server is running at http://localhost:8080") + slog.Error("Error starting Server", "Error", http.ListenAndServe(":8080", serverSessionMiddleware)) } func ParamsMiddleware(next http.HandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { queryParams := r.URL.Query() ctx := context.WithValue(r.Context(), "urlParams", queryParams) + slog.Debug("ParamsMiddleware added urlParams", slog.Any("urlParams", queryParams)) next.ServeHTTP(w, r.WithContext(ctx)) }) } diff --git a/Backend/models/absence.go b/Backend/models/absence.go index e3296a4..0f1f03b 100644 --- a/Backend/models/absence.go +++ b/Backend/models/absence.go @@ -22,6 +22,11 @@ type Absence struct { DateTo time.Time } +// IsEmpty implements [IWorkDay]. +func (a *Absence) IsEmpty() bool { + return false +} + func NewAbsence(card_uid string, abwesenheit_typ int, datum time.Time) (Absence, error) { if abwesenheit_typ < 0 { return Absence{ @@ -45,34 +50,57 @@ func (a *Absence) Date() time.Time { return a.Day.Truncate(24 * time.Hour) } +func (a *Absence) Type() DayType { + return DayTypeAbsence +} + func (a *Absence) IsMultiDay() bool { return !a.DateFrom.Equal(a.DateTo) } -func (a *Absence) TimeWorkVirtual(u User) time.Duration { - return a.TimeWorkReal(u) -} - -func (a *Absence) TimeWorkReal(u User) time.Duration { - if a.AbwesenheitTyp.WorkTime > 1 { - return time.Duration(u.ArbeitszeitPerTag * float32(time.Hour)).Round(time.Minute) +func (a *Absence) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + switch base { + case WorktimeBaseDay: + if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit { + return u.ArbeitszeitProTagFrac(1) + } else if a.AbwesenheitTyp.WorkTime <= 0 { + return 0 + } + return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100) + case WorktimeBaseWeek: + if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit { + return u.ArbeitszeitProTagFrac(0.2) + } else if a.AbwesenheitTyp.WorkTime <= 0 { + return 0 + } + return u.ArbeitszeitProWocheFrac(0.2 * float32(a.AbwesenheitTyp.WorkTime) / 100) } return 0 } -func (a *Absence) TimePauseReal(u User) (work, pause time.Duration) { - return 0, 0 +func (a *Absence) GetPausetime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + return 0 } -func (a *Absence) TimeOvertimeReal(u User) time.Duration { - if a.AbwesenheitTyp.WorkTime > 1 { +func (a *Absence) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + if a.AbwesenheitTyp.WorkTime > 0 { return 0 } - return -u.ArbeitszeitProTag() + switch base { + case WorktimeBaseDay: + return -u.ArbeitszeitProTagFrac(1) + case WorktimeBaseWeek: + return -u.ArbeitszeitProWocheFrac(0.2) + } + return 0 +} + +func (a *Absence) GetTimes(u User, base WorktimeBase, includeKurzarbeit bool) (work, pause, overtime time.Duration) { + return a.GetWorktime(u, base, includeKurzarbeit), a.GetPausetime(u, base, includeKurzarbeit), a.GetOvertime(u, base, includeKurzarbeit) } func (a *Absence) ToString() string { - return "Abwesenheit" + return a.AbwesenheitTyp.Name } func (a *Absence) IsWorkDay() bool { @@ -84,7 +112,7 @@ func (a *Absence) IsKurzArbeit() bool { } func (a *Absence) GetDayProgress(u User) int8 { - return 100 + return a.AbwesenheitTyp.WorkTime } func (a *Absence) RequiresAction() bool { @@ -266,3 +294,12 @@ func GetAbsenceTypeById(absenceTypeId int8) (AbsenceType, error) { } return absenceType, nil } + +func (a *Absence) Delete() error { + qStr, err := DB.Prepare("DELETE from abwesenheit WHERE counter_id = $1;") + if err != nil { + return err + } + _, err = qStr.Exec(a.CounterId) + return err +} diff --git a/Backend/models/absence_test.go b/Backend/models/absence_test.go new file mode 100644 index 0000000..235579b --- /dev/null +++ b/Backend/models/absence_test.go @@ -0,0 +1,92 @@ +package models_test + +import ( + "arbeitszeitmessung/helper" + "arbeitszeitmessung/models" + "testing" + "time" +) + +var testAbsence = models.Absence{ + Day: CatchError(time.Parse(time.DateOnly, "2025-01-01")), + AbwesenheitTyp: models.AbsenceType{}, + DateFrom: CatchError(time.Parse(time.DateOnly, "2025-01-01")), + DateTo: CatchError(time.Parse(time.DateOnly, "2025-01-03")), +} + +var testKurzarbeit = models.AbsenceType{ + Name: "Kurzarbeit", + WorkTime: -1, +} + +var testUrlaub = models.AbsenceType{ + Name: "Urlaub", + WorkTime: 100, +} + +var testUrlaubUntertags = models.AbsenceType{ + Name: "Urlaub untertags", + WorkTime: 50, +} + +func TestCalcRealWorkTimeDayAbsence(t *testing.T) { + testCases := []struct { + absenceType models.AbsenceType + expectedTime time.Duration + }{ + { + absenceType: testUrlaub, + expectedTime: time.Hour * 8, + }, + { + absenceType: testUrlaubUntertags, + expectedTime: time.Hour * 4, + }, + { + absenceType: testKurzarbeit, + expectedTime: 0, + }, + } + + for _, tc := range testCases { + t.Run("Calc Absence Worktime: "+tc.absenceType.Name, func(t *testing.T) { + var testCase = testAbsence + testCase.AbwesenheitTyp = tc.absenceType + workTime := testCase.GetWorktime(testUser, models.WorktimeBaseDay, false) + if workTime != tc.expectedTime { + t.Errorf("Calc Worktime Default not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true)) + } + }) + } +} + +func TestCalcRealWorkTimeWeekAbsence(t *testing.T) { + testCases := []struct { + absenceType models.AbsenceType + expectedTime time.Duration + }{ + { + absenceType: testUrlaub, + expectedTime: time.Hour * 7, + }, + { + absenceType: testUrlaubUntertags, + expectedTime: time.Hour*3 + time.Minute*30, + }, + { + absenceType: testKurzarbeit, + expectedTime: 0, + }, + } + + for _, tc := range testCases { + t.Run("Calc Absence Worktime: "+tc.absenceType.Name, func(t *testing.T) { + var testCase = testAbsence + testCase.AbwesenheitTyp = tc.absenceType + workTime := testCase.GetWorktime(testUser, models.WorktimeBaseWeek, false) + if workTime != tc.expectedTime { + t.Errorf("Calc Worktime Default not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true)) + } + }) + } +} diff --git a/Backend/models/booking.go b/Backend/models/booking.go index 7b4d356..28b638a 100644 --- a/Backend/models/booking.go +++ b/Backend/models/booking.go @@ -6,6 +6,7 @@ import ( "database/sql" "fmt" "log" + "log/slog" "net/url" "strconv" "time" @@ -87,6 +88,27 @@ func (b *Booking) Verify() bool { return true } +func (b *Booking) IsSubmittedAndChecked() bool { + qStr, err := DB.Prepare(`SELECT bestaetigt from wochen_report WHERE $1 = ANY(anwesenheiten);`) + if err != nil { + slog.Warn("Error when preparing SQL Statement", "error", err) + return false + } + defer qStr.Close() + var isSubmittedAndChecked bool = false + + err = qStr.QueryRow(b.CounterId).Scan(&isSubmittedAndChecked) + if err == sql.ErrNoRows { + // No rows found ==> not even submitted + return false + } + + if err != nil { + slog.Warn("Unexpected error when executing SQL Statement", "error", err) + } + return isSubmittedAndChecked +} + func (b *Booking) Insert() error { if !checkLastBooking(*b) { return SameBookingError{} diff --git a/Backend/models/booking_test.go b/Backend/models/booking_test.go index 032c59e..c5f3e67 100644 --- a/Backend/models/booking_test.go +++ b/Backend/models/booking_test.go @@ -10,36 +10,36 @@ var testBookingType = models.BookingType{ Name: "Büro", } -var testBookings8hrs = []models.Booking{models.Booking{ +var testBookings8hrs = []models.Booking{{ CardUID: "aaaa-aaaa", CheckInOut: 1, Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")), BookingType: testBookingType, -}, models.Booking{ +}, { CardUID: "aaaa-aaaa", CheckInOut: 2, Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 16:00")), BookingType: testBookingType, }} -var testBookings6hrs = []models.Booking{models.Booking{ +var testBookings6hrs = []models.Booking{{ CardUID: "aaaa-aaaa", CheckInOut: 1, Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")), BookingType: testBookingType, -}, models.Booking{ +}, { CardUID: "aaaa-aaaa", CheckInOut: 2, Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 14:00")), BookingType: testBookingType, }} -var testBookings10hrs = []models.Booking{models.Booking{ +var testBookings10hrs = []models.Booking{{ CardUID: "aaaa-aaaa", CheckInOut: 1, Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")), BookingType: testBookingType, -}, models.Booking{ +}, { CardUID: "aaaa-aaaa", CheckInOut: 2, Timestamp: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 18:00")), diff --git a/Backend/models/compoundDay.go b/Backend/models/compoundDay.go new file mode 100644 index 0000000..618d919 --- /dev/null +++ b/Backend/models/compoundDay.go @@ -0,0 +1,105 @@ +package models + +import ( + "log/slog" + "time" +) + +type CompoundDay struct { + Day time.Time + DayParts []IWorkDay +} + +func NewCompondDay(date time.Time, dayParts ...IWorkDay) *CompoundDay { + return &CompoundDay{Day: date, DayParts: dayParts} +} + +func (c *CompoundDay) AddDayPart(dayPart IWorkDay) { + c.DayParts = append(c.DayParts, dayPart) +} + +func (c *CompoundDay) GetWorkDay() WorkDay { + workday, ok := c.DayParts[0].(*WorkDay) + if ok { + return *workday + } + return WorkDay{} +} + +// IsEmpty implements [IWorkDay]. +func (c *CompoundDay) IsEmpty() bool { + return len(c.DayParts) > 0 +} + +// Date implements [IWorkDay]. +func (c *CompoundDay) Date() time.Time { + return c.Day +} + +// GetDayProgress implements [IWorkDay]. +func (c *CompoundDay) GetDayProgress(u User) int8 { + var dayProcess int8 + for _, day := range c.DayParts { + dayProcess += day.GetDayProgress(u) + } + return dayProcess +} + +// GetOvertime implements [IWorkDay]. +func (c *CompoundDay) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + + var overtime time.Duration + for _, day := range c.DayParts { + overtime += day.GetOvertime(u, base, includeKurzarbeit) + } + return overtime +} + +// GetPausetime implements [IWorkDay]. +func (c *CompoundDay) GetPausetime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + var pausetime time.Duration + for _, day := range c.DayParts { + pausetime += day.GetPausetime(u, base, includeKurzarbeit) + } + return pausetime +} + +// GetTimes implements [IWorkDay]. +func (c *CompoundDay) GetTimes(u User, base WorktimeBase, includeKurzarbeit bool) (work time.Duration, pause time.Duration, overtime time.Duration) { + return c.GetWorktime(u, base, includeKurzarbeit), c.GetPausetime(u, base, includeKurzarbeit), c.GetOvertime(u, base, includeKurzarbeit) +} + +// GetWorktime implements [IWorkDay]. +func (c *CompoundDay) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + var worktime time.Duration + for _, day := range c.DayParts { + worktime += day.GetWorktime(u, base, includeKurzarbeit) + slog.Info("Calc worktime for day", "day", day, "worktime", worktime.String()) + } + return worktime +} + +// IsKurzArbeit implements [IWorkDay]. +func (c *CompoundDay) IsKurzArbeit() bool { + return false +} + +// IsWorkDay implements [IWorkDay]. +func (c *CompoundDay) IsWorkDay() bool { + return true +} + +// RequiresAction implements [IWorkDay]. +func (c *CompoundDay) RequiresAction() bool { + return false +} + +// ToString implements [IWorkDay]. +func (c *CompoundDay) ToString() string { + return "Compound Day" +} + +// Type implements [IWorkDay]. +func (c *CompoundDay) Type() DayType { + return DayTypeCompound +} diff --git a/Backend/models/iworkday.go b/Backend/models/iworkday.go new file mode 100644 index 0000000..6305842 --- /dev/null +++ b/Backend/models/iworkday.go @@ -0,0 +1,91 @@ +package models + +import ( + "log/slog" + "time" +) + +type IWorkDay interface { + Date() time.Time + ToString() string + Type() DayType + IsWorkDay() bool + IsKurzArbeit() bool + GetDayProgress(User) int8 + RequiresAction() bool + GetWorktime(User, WorktimeBase, bool) time.Duration + GetPausetime(User, WorktimeBase, bool) time.Duration + GetTimes(User, WorktimeBase, bool) (work, pause, overtime time.Duration) + GetOvertime(User, WorktimeBase, bool) time.Duration + IsEmpty() bool +} + +type DayType int + +const ( + DayTypeWorkday DayType = 1 + DayTypeAbsence DayType = 2 + DayTypeHoliday DayType = 3 + DayTypeCompound DayType = 4 +) + +func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay { + var allDays map[string]IWorkDay = make(map[string]IWorkDay) + + workdays := GetWorkDays(user, tsFrom, tsTo) + absences, err := GetAbsencesByCardUID(user.CardUID, tsFrom, tsTo) + if err != nil { + slog.Warn("Error gettings absences!", slog.Any("Error", err)) + return nil + } + holidays, err := GetHolidaysFromTo(tsFrom, tsTo) + if err != nil { + slog.Warn("Error getting holidays!", slog.Any("Error", err)) + return nil + } + + for _, day := range workdays { + allDays[day.Date().Format(time.DateOnly)] = &day + } + + for _, absentDay := range absences { + // Kurzarbeit should be integrated in workday + existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)] + if !ok { + allDays[absentDay.Date().Format(time.DateOnly)] = &absentDay + continue + } + switch { + case absentDay.AbwesenheitTyp.WorkTime < 0: + if workDay, ok := allDays[absentDay.Date().Format(time.DateOnly)].(*WorkDay); ok { + workDay.kurzArbeit = true + workDay.kurzArbeitAbsence = absentDay + } + case !existingDay.IsEmpty(): + allDays[absentDay.Date().Format(time.DateOnly)] = NewCompondDay(absentDay.Date(), existingDay, &absentDay) + default: + allDays[absentDay.Date().Format(time.DateOnly)] = &absentDay + } + } + + for _, holiday := range holidays { + existingDay, ok := allDays[holiday.Date().Format(time.DateOnly)] + if !ok { + allDays[holiday.Date().Format(time.DateOnly)] = &holiday + continue + } + slog.Info("Existing Day", "day", existingDay) + switch { + case existingDay.Type() == DayTypeCompound: + allDays[holiday.Date().Format(time.DateOnly)].(*CompoundDay).AddDayPart(&holiday) + case existingDay.Type() != DayTypeCompound && !existingDay.IsEmpty(): + allDays[holiday.Date().Format(time.DateOnly)] = NewCompondDay(holiday.Date(), existingDay, &holiday) + default: + allDays[holiday.Date().Format(time.DateOnly)] = &holiday + } + slog.Debug("Logging Holiday: ", slog.String("HolidayName", allDays[holiday.Date().Format(time.DateOnly)].ToString()), slog.Any("Overtime", holiday.GetOvertime(user, WorktimeBaseDay, false).String()), "wokrtie", float32(holiday.worktime)/100) + } + + sortedDays := sortDays(allDays, orderedForward) + return sortedDays +} diff --git a/Backend/models/publicHoliday.go b/Backend/models/publicHoliday.go new file mode 100644 index 0000000..567a610 --- /dev/null +++ b/Backend/models/publicHoliday.go @@ -0,0 +1,137 @@ +package models + +import ( + "time" + + "github.com/wlbr/feiertage" +) + +// type PublicHoliday feiertage.Feiertag + +type PublicHoliday struct { + feiertage.Feiertag + repeat int8 + worktime int8 +} + +// IsEmpty implements [IWorkDay]. +func (p *PublicHoliday) IsEmpty() bool { + return false +} + +func NewHolidayFromFeiertag(f feiertage.Feiertag) PublicHoliday { + return PublicHoliday{ + Feiertag: f, + repeat: 0, + worktime: 100, + } +} + +func GetHolidaysFromTo(tsFrom, tsTo time.Time) ([]PublicHoliday, error) { + var publicHolidays []PublicHoliday + qStr, err := DB.Prepare(`SELECT datum, name, wiederholen, arbeitszeit_equivalent FROM s_feiertage WHERE datum::DATE >= $1::DATE AND datum::DATE <= $2::DATE;`) + if err != nil { + return publicHolidays, err + } + + rows, err := qStr.Query(tsFrom, tsTo) + if err != nil { + return publicHolidays, err + } + defer rows.Close() + for rows.Next() { + var publicHoliday PublicHoliday + if err := rows.Scan(&publicHoliday.Time, &publicHoliday.Text, &publicHoliday.repeat, &publicHoliday.worktime); err != nil { + return publicHolidays, err + } + publicHolidays = append(publicHolidays, publicHoliday) + } + return publicHolidays, nil +} + +func GetRepeatingHolidays(tsFrom, tsTo time.Time) ([]PublicHoliday, error) { + var publicHolidays []PublicHoliday + qStr, err := DB.Prepare(`SELECT datum, name, wiederholen, arbeitszeit_equivalent FROM s_feiertage WHERE wiederholen = 1 AND datum::DATE >= $1::DATE AND datum::DATE <= $2::DATE;`) + if err != nil { + return publicHolidays, err + } + rows, err := qStr.Query(tsFrom, tsTo) + if err != nil { + return publicHolidays, err + } + defer rows.Close() + for rows.Next() { + var publicHoliday PublicHoliday + if err := rows.Scan(&publicHoliday.Time, &publicHoliday.Text, &publicHoliday.repeat, &publicHoliday.worktime); err != nil { + return publicHolidays, err + } + publicHolidays = append(publicHolidays, publicHoliday) + } + return publicHolidays, nil + +} +func (p *PublicHoliday) Insert() error { + qStr, err := DB.Prepare(`INSERT INTO s_feiertage(name, datum, wiederholen, arbeitszeit_equivalent) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING;`) + if err != nil { + return err + } + _, err = qStr.Exec(p.Text, p.Time, p.repeat, p.worktime) + return err +} + +func (p *PublicHoliday) Type() DayType { + return DayTypeHoliday +} + +// Interface implementation +func (p *PublicHoliday) Date() time.Time { + return p.Time +} + +func (p *PublicHoliday) ToString() string { + return p.Text +} + +func (p *PublicHoliday) IsWorkDay() bool { + return false +} + +func (p *PublicHoliday) IsKurzArbeit() bool { + return false +} + +func (p *PublicHoliday) GetDayProgress(User) int8 { + return p.worktime +} + +func (p *PublicHoliday) RequiresAction() bool { + return false +} + +func (p *PublicHoliday) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + switch base { + case WorktimeBaseDay: + return u.ArbeitszeitProTagFrac(float32(p.worktime) / 100) + case WorktimeBaseWeek: + return u.ArbeitszeitProWocheFrac(float32(p.worktime) / 500) + } + return 0 +} + +func (p *PublicHoliday) GetPausetime(User, WorktimeBase, bool) time.Duration { + return 0 +} + +func (p *PublicHoliday) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + switch base { + case WorktimeBaseDay: + return u.ArbeitszeitProTagFrac(float32(p.worktime)/100) - u.ArbeitszeitProTagFrac(1) + case WorktimeBaseWeek: + return u.ArbeitszeitProWocheFrac(float32(p.worktime)/500) - u.ArbeitszeitProWocheFrac(0.2) + } + return 0 +} + +func (p *PublicHoliday) GetTimes(u User, base WorktimeBase, includeKurzarbeit bool) (work, pause, overtime time.Duration) { + return p.GetWorktime(u, base, includeKurzarbeit), 0, 0 +} diff --git a/Backend/models/user.go b/Backend/models/user.go index e3d94a9..5cfb559 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -7,9 +7,11 @@ import ( "errors" "fmt" "log" + "log/slog" "time" "github.com/alexedwards/scs/v2" + "github.com/lib/pq" ) type User struct { @@ -57,11 +59,37 @@ func (u *User) GetReportedOvertime() (time.Duration, error) { return overtime, nil } +func GetAllUsers() ([]User, error) { + qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname,arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten;`)) + var users []User + if err != nil { + return users, err + } + defer qStr.Close() + rows, err := qStr.Query() + if err != nil { + return users, err + } + defer rows.Close() + for rows.Next() { + + var user User + if err := rows.Scan(&user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche); err != nil { + log.Println("Error creating user!", err) + continue + } + users = append(users, user) + } + if err = rows.Err(); err != nil { + return users, nil + } + return users, nil +} + func (u *User) GetAll() ([]User, error) { 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) return users, err } defer qStr.Close() @@ -85,13 +113,21 @@ func (u *User) GetAll() ([]User, error) { return users, nil } -// Returns the worktime per day rounded to minutes func (u *User) ArbeitszeitProTag() time.Duration { - return time.Duration(u.ArbeitszeitPerTag * float32(time.Hour)).Round(time.Minute) + return u.ArbeitszeitProTagFrac(1) +} + +// Returns the worktime per day rounded to minutes +func (u *User) ArbeitszeitProTagFrac(fraction float32) time.Duration { + return time.Duration(u.ArbeitszeitPerTag * float32(time.Hour) * fraction).Round(time.Minute) } func (u *User) ArbeitszeitProWoche() time.Duration { - return time.Duration(u.ArbeitszeitPerWoche * float32(time.Hour)).Round(time.Minute) + return u.ArbeitszeitProWocheFrac(1) +} + +func (u *User) ArbeitszeitProWocheFrac(fraction float32) time.Duration { + return time.Duration(u.ArbeitszeitPerWoche * float32(time.Hour) * fraction).Round(time.Minute) } // Returns true if there is a booking 1 for today -> meaning the user is at work @@ -99,7 +135,7 @@ func (u *User) ArbeitszeitProWoche() time.Duration { func (u *User) CheckAnwesenheit() bool { qStr, err := DB.Prepare((`SELECT check_in_out FROM anwesenheit WHERE card_uid = $1 AND "timestamp"::date = now()::date ORDER BY "timestamp" DESC LIMIT 1;`)) if err != nil { - fmt.Printf("Error preparing query statement %v\n", err) + slog.Debug("Error preparing query statement.", "error", err) return false } defer qStr.Close() @@ -137,11 +173,43 @@ func GetUserByPersonalNr(personalNummer int) (User, error) { return user, nil } +func GetUserByPersonalNrMulti(personalNummerMulti []int) ([]User, error) { + var users []User + if len(personalNummerMulti) == 0 { + return users, errors.New("No personalNumbers provided") + } + + qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten WHERE personal_nummer = ANY($1::int[]);`)) + if err != nil { + return users, err + } + + rows, err := qStr.Query(pq.Array(personalNummerMulti)) + if err == sql.ErrNoRows { + return users, err + } + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var user User + if err := rows.Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche); err != nil { + return users, err + } + users = append(users, user) + } + if err = rows.Err(); err != nil { + return users, err + } + return users, nil +} + func (u *User) Login(password string) bool { var loginSuccess bool qStr, err := DB.Prepare((`SELECT (pass_hash = crypt($2, pass_hash)) AS pass_hash FROM user_password WHERE personal_nummer = $1;`)) if err != nil { - log.Println("Error preparing db statement", err) + slog.Debug("Error preparing query statement.", "error", err) return false } defer qStr.Close() @@ -233,7 +301,7 @@ func (u *User) GetLastWorkWeekSubmission() time.Time { ) AS letzte_buchung; `) if err != nil { - log.Println("Error preparing statement!", err) + slog.Debug("Error preparing query statement.", "error", err) return lastSub } err = qStr.QueryRow(u.PersonalNummer, u.CardUID).Scan(&lastSub) @@ -262,6 +330,22 @@ func (u *User) GetFromCardUID(card_uid string) (User, error) { return user, nil } +func (u *User) IsSuperior(e User) bool { + var isSuperior int + qStr, err := DB.Prepare(`SELECT COUNT(1) FROM s_personal_daten WHERE personal_nummer = $1 AND vorgesetzter_pers_nr = $2`) + if err != nil { + slog.Debug("Error preparing query", "error", err) + return false + } + err = qStr.QueryRow(e.PersonalNummer, u.PersonalNummer).Scan(&isSuperior) + if err != nil { + slog.Debug("Error executing query", "error", err) + return false + } + return isSuperior == 1 + +} + func getMonday(ts time.Time) time.Time { if ts.Weekday() != time.Monday { if ts.Weekday() == time.Sunday { diff --git a/Backend/models/user_test.go b/Backend/models/user_test.go index c214625..0fe0a71 100644 --- a/Backend/models/user_test.go +++ b/Backend/models/user_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -var testUser models.User = models.User{Vorname: "Kim", Name: "Mustermensch", PersonalNummer: 456, CardUID: "aaaa-aaaa", ArbeitszeitPerTag: 8, ArbeitszeitPerWoche: 40} +var testUser models.User = models.User{Vorname: "Kim", Name: "Mustermensch", PersonalNummer: 456, CardUID: "aaaa-aaaa", ArbeitszeitPerTag: 8, ArbeitszeitPerWoche: 35} func SetupUserFixture(t *testing.T, db models.IDatabase) { t.Helper() diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index 9780ce7..c995275 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -5,68 +5,117 @@ import ( "encoding/json" "fmt" "log" + "log/slog" "sort" "time" ) -type IWorkDay interface { - Date() time.Time - TimeWorkVirtual(User) time.Duration - TimeWorkReal(User) time.Duration - TimePauseReal(User) (work, pause time.Duration) - TimeOvertimeReal(User) time.Duration - GetAllWorkTimesVirtual(User) (work, pause, overtime time.Duration) - ToString() string - IsWorkDay() bool - IsKurzArbeit() bool - GetDayProgress(User) int8 - RequiresAction() bool -} - type WorkDay struct { Day time.Time `json:"day"` Bookings []Booking `json:"bookings"` workTime time.Duration pauseTime time.Duration - realWorkTime time.Duration - realPauseTime time.Duration TimeFrom time.Time TimeTo time.Time kurzArbeit bool kurzArbeitAbsence Absence + // Urlaub untertags + worktimeAbsece Absence } -func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay { - var allDays map[string]IWorkDay = make(map[string]IWorkDay) - var sortedDays []IWorkDay - for _, day := range GetWorkDays(user, tsFrom, tsTo) { - allDays[day.Date().Format("2006-01-02")] = &day +// IsEmpty implements [IWorkDay]. +func (d *WorkDay) IsEmpty() bool { + return len(d.Bookings) == 0 +} + +type WorktimeBase int + +const ( + WorktimeBaseWeek WorktimeBase = 5 + WorktimeBaseDay WorktimeBase = 1 +) + +func (d *WorkDay) GetWorktimeAbsence() Absence { + return d.worktimeAbsece +} + +// Gets the time as is in the db (with corrected pause times) +func (d *WorkDay) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + if includeKurzarbeit && d.IsKurzArbeit() && len(d.Bookings) > 0 { + return d.kurzArbeitAbsence.GetWorktime(u, base, true) } - absences, err := GetAbsencesByCardUID(user.CardUID, tsFrom, tsTo) - if err != nil { - log.Println("Error gettings absences for all Days!", err) - return sortedDays + work, pause := calcWorkPause(d.Bookings) + work, pause = correctWorkPause(work, pause) + if (d.worktimeAbsece != Absence{}) { + work += d.worktimeAbsece.GetWorktime(u, base, false) } - for _, day := range absences { - if helper.IsWeekend(day.Date()) { - continue - } - if day.AbwesenheitTyp.WorkTime == 1 { - if workDay, ok := allDays[day.Date().Format("2006-01-02")].(*WorkDay); ok { - if len(workDay.Bookings) > 0 { - workDay.kurzArbeit = true - workDay.kurzArbeitAbsence = day - } + return work.Round(time.Minute) +} + +// Gets the corrected pause times based on db entries +func (d *WorkDay) GetPausetime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + work, pause := calcWorkPause(d.Bookings) + work, pause = correctWorkPause(work, pause) + return pause.Round(time.Minute) +} + +// Returns the overtime based on the db entries +func (d *WorkDay) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + work := d.GetWorktime(u, base, includeKurzarbeit) + var targetHours time.Duration + switch base { + case WorktimeBaseDay: + targetHours = u.ArbeitszeitProTag() + case WorktimeBaseWeek: + targetHours = u.ArbeitszeitProWocheFrac(0.2) + } + return (work - targetHours).Round(time.Minute) +} + +func (d *WorkDay) GetTimes(u User, base WorktimeBase, includeKurzarbeit bool) (work, pause, overtime time.Duration) { + return d.GetWorktime(u, base, includeKurzarbeit), d.GetPausetime(u, base, includeKurzarbeit), d.GetOvertime(u, base, includeKurzarbeit) +} + +func calcWorkPause(bookings []Booking) (work, pause time.Duration) { + var lastBooking Booking + for _, b := range bookings { + if b.CheckInOut%2 == 1 { + if !lastBooking.Timestamp.IsZero() { + pause += b.Timestamp.Sub(lastBooking.Timestamp) } } else { - allDays[day.Date().Format("2006-01-02")] = &day + work += b.Timestamp.Sub(lastBooking.Timestamp) } + lastBooking = b + } + if len(bookings)%2 == 1 { + work += time.Since(lastBooking.Timestamp.Local()) + } + return work, pause +} + +func correctWorkPause(workIn, pauseIn time.Duration) (work, pause time.Duration) { + if workIn <= 6*time.Hour || pauseIn > 45*time.Minute { + return workIn, pauseIn } - for _, day := range allDays { + var diff time.Duration + if workIn <= (9*time.Hour) && pauseIn < 30*time.Minute { + diff = 30*time.Minute - pauseIn + } else if pauseIn < 45*time.Minute { + diff = 45*time.Minute - pauseIn + } + work = workIn - diff + pause = pauseIn + diff + return work, pause +} + +func sortDays(days map[string]IWorkDay, forward bool) []IWorkDay { + var sortedDays []IWorkDay + for _, day := range days { sortedDays = append(sortedDays, day) } - if orderedForward { + if forward { sort.Slice(sortedDays, func(i, j int) bool { return sortedDays[i].Date().After(sortedDays[j].Date()) }) @@ -82,72 +131,29 @@ func (d *WorkDay) Date() time.Time { return d.Day } -func (d *WorkDay) TimeWorkVirtual(u User) time.Duration { - if d.IsKurzArbeit() { - return u.ArbeitszeitProTag() +func (d *WorkDay) Type() DayType { + return DayTypeWorkday +} + +func (d *WorkDay) GenerateKurzArbeitBookings(u User) (time.Time, time.Time) { + var timeFrom, timeTo time.Time + if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() { + return timeFrom, timeTo } - return d.workTime + timeFrom = d.Bookings[len(d.Bookings)-1].Timestamp.Add(time.Minute) + timeTo = timeFrom.Add(u.ArbeitszeitProTag() - d.GetWorktime(u, WorktimeBaseDay, false)) + slog.Debug("Added duration as Kurzarbeit", "date", d.Date().String(), "duration", timeTo.Sub(timeFrom).String()) + + return timeFrom, timeTo } func (d *WorkDay) GetKurzArbeit() *Absence { return &d.kurzArbeitAbsence } -func (d *WorkDay) TimeWorkReal(u User) time.Duration { - d.realWorkTime, d.realPauseTime = 0, 0 - var lastBooking Booking - for _, booking := range d.Bookings { - if booking.CheckInOut%2 == 1 { - if !lastBooking.Timestamp.IsZero() { - d.realPauseTime += booking.Timestamp.Sub(lastBooking.Timestamp) - } - } else { - d.realWorkTime += booking.Timestamp.Sub(lastBooking.Timestamp) - } - lastBooking = booking - } - if helper.IsSameDate(d.Date(), time.Now()) && len(d.Bookings)%2 == 1 { - d.realWorkTime += time.Since(lastBooking.Timestamp.Local()) - } - return d.realWorkTime -} - -func (d *WorkDay) TimeOvertimeReal(u User) time.Duration { - workTime := d.TimeWorkVirtual(u) - if workTime == 0 { - workTime, _ = d.TimePauseReal(u) - } - if helper.IsWeekend(d.Day) && len(d.Bookings) == 0 { - return 0 - } - var overtime time.Duration - overtime = workTime - u.ArbeitszeitProTag() - return overtime -} - -func (d *WorkDay) TimePauseReal(u User) (work, pause time.Duration) { - if d.realWorkTime == 0 { - d.TimeWorkReal(u) - } - d.workTime, d.pauseTime = d.realWorkTime, d.realPauseTime - if d.realWorkTime <= 6*time.Hour || d.realPauseTime > 45*time.Minute { - return d.realWorkTime, d.realPauseTime - } - if d.realWorkTime <= (9*time.Hour) && d.realPauseTime < 30*time.Minute { - diff := 30*time.Minute - d.pauseTime - d.workTime -= diff - d.pauseTime += diff - } else if d.realPauseTime < 45*time.Minute { - diff := 45*time.Minute - d.pauseTime - d.workTime -= diff - d.pauseTime += diff - } - return d.workTime, d.pauseTime -} - func (d *WorkDay) ToString() string { - return fmt.Sprintf("WorkDay: %s with %d bookings and worktime: %s", d.Date().Format("2006-01-02"), len(d.Bookings), helper.FormatDuration(d.workTime)) + return fmt.Sprintf("WorkDay: %s with %d bookings and worktime: %s", d.Date().Format(time.DateOnly), len(d.Bookings), helper.FormatDuration(d.workTime)) } func (d *WorkDay) IsWorkDay() bool { @@ -234,7 +240,6 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay { return workDays } defer rows.Close() - // emptyDays, _ := strconv.ParseBool(helper.GetEnv("EMPTY_DAYS", "false")) for rows.Next() { var workDay WorkDay var bookings []byte @@ -253,7 +258,6 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay { if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 { workDay.Bookings = []Booking{} } - workDay.TimePauseReal(user) if len(workDay.Bookings) > 1 || !helper.IsWeekend(workDay.Date()) { workDays = append(workDays, workDay) } @@ -265,18 +269,6 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay { return workDays } -func (d *WorkDay) GetAllWorkTimesReal(user User) (work, pause, overtime time.Duration) { - if d.pauseTime == 0 || d.workTime == 0 { - d.TimePauseReal(user) - } - return d.workTime.Round(time.Minute), d.pauseTime.Round(time.Minute), d.TimeOvertimeReal(user) -} - -func (d *WorkDay) GetAllWorkTimesVirtual(user User) (work, pause, overtime time.Duration) { - _, pause, overtime = d.GetAllWorkTimesReal(user) - return d.TimeWorkVirtual(user), pause, overtime -} - // returns bool wheter the workday was ended with an automatic logout func (d *WorkDay) RequiresAction() bool { if len(d.Bookings) == 0 { @@ -289,19 +281,7 @@ func (d *WorkDay) GetDayProgress(u User) int8 { if d.RequiresAction() { return -1 } - workTime := d.TimeWorkVirtual(u) + workTime := d.GetWorktime(u, WorktimeBaseDay, true) progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100 return int8(progress) } - -// func (d *WorkDay) CalcOvertime(user User) time.Duration { -// if d.workTime == 0 { -// d.TimePauseReal(user) -// } -// if helper.IsWeekend(d.Day) && len(d.Bookings) == 0 { -// return 0 -// } -// var overtime time.Duration -// overtime = d.workTime - user.ArbeitszeitProTag() -// return overtime -// } diff --git a/Backend/models/workDay_test.go b/Backend/models/workDay_test.go index abc3d89..9ad0e6d 100644 --- a/Backend/models/workDay_test.go +++ b/Backend/models/workDay_test.go @@ -16,65 +16,147 @@ func CatchError[T any](val T, err error) T { } var testWorkDay = models.WorkDay{ - Day: CatchError(time.Parse("2006-01-02", "2025-01-01")), + Day: CatchError(time.Parse(time.DateOnly, "2025-01-01")), Bookings: testBookings8hrs, TimeFrom: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 08:00")), TimeTo: CatchError(time.Parse("2006-01-02 15:04", "2025-01-01 16:30")), } -func TestCalcRealWorkTime(t *testing.T) { - workTime := testWorkDay.TimeWorkReal(testUser) - if workTime != time.Hour*8 { - t.Errorf("Calc Worktime Default not working, time should be 8h, but was %s", helper.FormatDuration(workTime)) - } -} - -func TestCalcWorkPauseDiff(t *testing.T) { - type testCase struct { - Name string - bookings []models.Booking - expectedWorkTime time.Duration - expectedPauseTime time.Duration - expectedOvertime time.Duration - } - - testCases := []testCase{testCase{ - Name: "6hrs no pause", - bookings: testBookings6hrs, - expectedWorkTime: 6 * time.Hour, - expectedPauseTime: 0, - expectedOvertime: -2 * time.Hour, - }, - testCase{ - Name: "8hrs - 30min pause", - bookings: testBookings8hrs, - expectedWorkTime: 7*time.Hour + 30*time.Minute, - expectedPauseTime: 30 * time.Minute, - expectedOvertime: -30 * time.Minute, +func TestWorkdayWorktimeDay(t *testing.T) { + testCases := []struct { + testName string + bookings []models.Booking + expectedTime time.Duration + }{ + { + testName: "Bookings6hrs", + bookings: testBookings6hrs, + expectedTime: time.Hour * 6, }, - testCase{ - Name: "10hrs - 45min pause", - bookings: testBookings10hrs, - expectedWorkTime: 9*time.Hour + 15*time.Minute, - expectedPauseTime: 45 * time.Minute, - expectedOvertime: 1*time.Hour + 15*time.Minute, - }} + { + testName: "Bookings8hrs", + bookings: testBookings8hrs, + expectedTime: time.Hour*7 + time.Minute*30, + }, + { + testName: "Bookings10hrs", + bookings: testBookings10hrs, + expectedTime: time.Hour*9 + time.Minute*15, + }, + } - for _, test := range testCases { - t.Run(test.Name, func(t *testing.T) { - testWorkDay.Bookings = test.bookings - testWorkDay.TimeWorkReal(testUser) - testWorkDay.TimePauseReal(testUser) - testWorkDay.TimeOvertimeReal(testUser) - workTime, pauseTime, overTime := testWorkDay.GetAllWorkTimesReal(testUser) - if workTime != test.expectedWorkTime { - t.Errorf("Calculated wrong workTime: should be %s, but was %s", helper.FormatDuration(test.expectedWorkTime), helper.FormatDuration(workTime)) - } - if pauseTime != test.expectedPauseTime { - t.Errorf("Calculated wrong pauseTime: should be %s, but was %s", helper.FormatDuration(test.expectedPauseTime), helper.FormatDuration(pauseTime)) - } - if overTime != test.expectedOvertime { - t.Errorf("Calculated wrong overtime: should be %s, but was %s", helper.FormatDuration(test.expectedOvertime), helper.FormatDuration(overTime)) + for _, tc := range testCases { + t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) { + var testCase = testWorkDay + testCase.Bookings = tc.bookings + workTime := testCase.GetWorktime(testUser, models.WorktimeBaseDay, false) + if workTime != tc.expectedTime { + t.Errorf("GetWorktimeReal not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true)) + } + }) + } +} + +func TestWorkdayWorktimeWeek(t *testing.T) { + testCases := []struct { + testName string + bookings []models.Booking + expectedTime time.Duration + }{ + { + testName: "Bookings6hrs", + bookings: testBookings6hrs, + expectedTime: time.Hour * 6, + }, + { + testName: "Bookings8hrs", + bookings: testBookings8hrs, + expectedTime: time.Hour*7 + time.Minute*30, + }, + { + testName: "Bookings10hrs", + bookings: testBookings10hrs, + expectedTime: time.Hour*9 + time.Minute*15, + }, + } + + for _, tc := range testCases { + t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) { + var testCase = testWorkDay + testCase.Bookings = tc.bookings + workTime := testCase.GetWorktime(testUser, models.WorktimeBaseWeek, false) + if workTime != tc.expectedTime { + t.Errorf("GetWorktimeReal not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true)) + } + }) + } +} + +func TestWorkdayPausetimeDay(t *testing.T) { + testCases := []struct { + testName string + bookings []models.Booking + expectedTime time.Duration + }{ + { + testName: "Bookings6hrs", + bookings: testBookings6hrs, + expectedTime: 0, + }, + { + testName: "Bookings8hrs", + bookings: testBookings8hrs, + expectedTime: time.Minute * 30, + }, + { + testName: "Bookings10hrs", + bookings: testBookings10hrs, + expectedTime: time.Minute * 45, + }, + } + + for _, tc := range testCases { + t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) { + var testCase = testWorkDay + testCase.Bookings = tc.bookings + workTime := testCase.GetPausetime(testUser, models.WorktimeBaseDay, false) + if workTime != tc.expectedTime { + t.Errorf("GetPausetimeReal not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true)) + } + }) + } +} + +func TestWorkdayPausetimeWeek(t *testing.T) { + testCases := []struct { + testName string + bookings []models.Booking + expectedTime time.Duration + }{ + { + testName: "Bookings6hrs", + bookings: testBookings6hrs, + expectedTime: 0, + }, + { + testName: "Bookings8hrs", + bookings: testBookings8hrs, + expectedTime: time.Minute * 30, + }, + { + testName: "Bookings10hrs", + bookings: testBookings10hrs, + expectedTime: time.Minute * 45, + }, + } + + for _, tc := range testCases { + t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) { + var testCase = testWorkDay + testCase.Bookings = tc.bookings + workTime := testCase.GetPausetime(testUser, models.WorktimeBaseWeek, false) + if workTime != tc.expectedTime { + t.Errorf("GetPausetimeReal not working, time should be %s, but was %s", helper.FormatDurationFill(tc.expectedTime, true), helper.FormatDurationFill(workTime, true)) } }) } diff --git a/Backend/models/workWeek.go b/Backend/models/workWeek.go index 634d6fb..168365a 100644 --- a/Backend/models/workWeek.go +++ b/Backend/models/workWeek.go @@ -4,23 +4,25 @@ import ( "database/sql" "errors" "log" + "log/slog" "time" + + "github.com/lib/pq" ) // Workweeks are type WorkWeek struct { - Id int - WorkDays []WorkDay - Absences []Absence - Days []IWorkDay - User User - WeekStart time.Time - Worktime time.Duration - Overtime time.Duration - Status WeekStatus - overtimeDiff time.Duration - worktimeDiff time.Duration + Id int + WorkDays []WorkDay + Absences []Absence + Days []IWorkDay + User User + WeekStart time.Time + Worktime time.Duration + WorktimeVirtual time.Duration + Overtime time.Duration + Status WeekStatus } type WeekStatus int8 @@ -45,16 +47,19 @@ func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek { } func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Duration) { - log.Println("Got Days with overtime and worktime", worktime, overtime) + slog.Debug("Populating Workweek for user", "user", w.User) + slog.Debug("Got Days with overtime and worktime", slog.String("worktime", worktime.String()), slog.String("overtime", overtime.String())) w.Days = GetDays(w.User, w.WeekStart, w.WeekStart.Add(6*24*time.Hour), false) - log.Println(w.Worktime) for _, day := range w.Days { - log.Println(day.TimeWorkVirtual(w.User)) - w.Worktime += day.TimeWorkVirtual(w.User) + w.Worktime += day.GetWorktime(w.User, WorktimeBaseDay, false) + w.WorktimeVirtual += day.GetWorktime(w.User, WorktimeBaseDay, true) } - log.Println("Calculated new worktime", w.Worktime) - w.Overtime = w.Worktime - w.User.ArbeitszeitProWoche() + slog.Debug("Got worktime for user", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorktimeVirtual.String()) + + w.Overtime = w.WorktimeVirtual - w.User.ArbeitszeitProWoche() + + slog.Debug("Calculated overtime", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorktimeVirtual.String()) w.Worktime = w.Worktime.Round(time.Minute) w.Overtime = w.Overtime.Round(time.Minute) @@ -65,8 +70,6 @@ func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Durati if overtime != w.Overtime || worktime != w.Worktime { w.Status = WeekStatusDifferences - w.overtimeDiff = overtime - w.worktimeDiff = worktime } } @@ -149,30 +152,70 @@ func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek { var ErrRunningWeek = errors.New("Week is in running week") +func (w *WorkWeek) GetBookingIds() (anwesenheitsIds, abwesenheitsIds []int64, err error) { + qStr, err := DB.Prepare(` + SELECT + (SELECT array_agg(counter_id ORDER BY counter_id) + FROM anwesenheit + WHERE card_uid = $1 + AND timestamp::DATE >= $2 + AND timestamp::DATE < $3) AS anwesenheit, + + (SELECT array_agg(counter_id ORDER BY counter_id) + FROM abwesenheit + WHERE card_uid = $1 + AND datum_from < $3 + AND datum_to >= $2) AS abwesenheit; + `) + if err != nil { + return nil, nil, err + } + defer qStr.Close() + + slog.Debug("Inserting parameters into qStr:", "user card_uid", w.User.CardUID, "week_start", w.WeekStart, "week_end", w.WeekStart.AddDate(0, 0, 5)) + + err = qStr.QueryRow(w.User.CardUID, w.WeekStart, w.WeekStart.AddDate(0, 0, 5)).Scan(pq.Array(&anwesenheitsIds), pq.Array(&abwesenheitsIds)) + if err != nil { + return anwesenheitsIds, abwesenheitsIds, err + } + return anwesenheitsIds, abwesenheitsIds, nil +} + // creates a new entry in the woche_report table with the given workweek func (w *WorkWeek) SendWeek() error { var qStr *sql.Stmt var err error + slog.Info("Sending workWeek to team head", "week", w.WeekStart.String()) + + anwBookings, awBookings, err := w.GetBookingIds() + if err != nil { + slog.Warn("Error querying bookings from work week", slog.Any("error", err)) + return err + } + + slog.Debug("Recieved Booking Ids", "anwesenheiten", anwBookings) + if time.Since(w.WeekStart) < 5*24*time.Hour { - log.Println("Cannot send week, because it's the running week!") + slog.Warn("Cannot send week, because it's the running week!") return ErrRunningWeek } if w.CheckStatus() != WeekStatusNone { - qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE, arbeitszeit = make_interval(secs => $3::numeric / 1000000000), ueberstunden = make_interval(secs => $4::numeric / 1000000000) WHERE personal_nummer = $1 AND woche_start = $2;`) + qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE, arbeitszeit = make_interval(secs => $3::numeric / 1000000000), ueberstunden = make_interval(secs => $4::numeric / 1000000000), anwesenheiten=$5, abwesenheiten=$6 WHERE personal_nummer = $1 AND woche_start = $2;`) if err != nil { - log.Println("Error preparing SQL statement", err) + slog.Warn("Error preparing SQL statement", "error", err) return err } } else { - qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden) VALUES ($1, $2, make_interval(secs => $3::numeric / 1000000000), make_interval(secs => $4::numeric / 1000000000));`) + qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden, anwesenheiten, abwesenheiten) VALUES ($1, $2, make_interval(secs => $3::numeric / 1000000000), make_interval(secs => $4::numeric / 1000000000), $5, $6);`) if err != nil { - log.Println("Error preparing SQL statement", err) + slog.Warn("Error preparing SQL statement", "error", err) return err } } - _, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, int64(w.Worktime), int64(w.Overtime)) + + _, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, int64(w.Worktime), int64(w.Overtime), pq.Array(anwBookings), pq.Array(awBookings)) if err != nil { log.Println("Error executing query!", err) return err diff --git a/Backend/models/workWeek_test.go b/Backend/models/workWeek_test.go index cd78cef..ef989e4 100644 --- a/Backend/models/workWeek_test.go +++ b/Backend/models/workWeek_test.go @@ -8,7 +8,7 @@ import ( func SetupWorkWeekFixture(t *testing.T) models.WorkWeek { t.Helper() - monday, err := time.Parse("2006-01-02", "2025-01-10") + monday, err := time.Parse(time.DateOnly, "2025-01-10") if err != nil { t.Fatal(err) } @@ -16,7 +16,7 @@ func SetupWorkWeekFixture(t *testing.T) models.WorkWeek { } func TestNewWorkWeekNoPopulate(t *testing.T) { - monday, err := time.Parse("2006-01-02", "2025-01-10") + monday, err := time.Parse(time.DateOnly, "2025-01-10") if err != nil { t.Fatal(err) } diff --git a/Backend/static/css/styles.css b/Backend/static/css/styles.css index 8b42ab0..6d0ccdd 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -12,7 +12,9 @@ --color-red-700: oklch(50.5% 0.213 27.518); --color-orange-500: oklch(70.5% 0.213 47.604); --color-purple-600: oklch(55.8% 0.288 302.321); + --color-slate-600: oklch(44.6% 0.043 257.281); --color-slate-700: oklch(37.2% 0.044 257.287); + --color-slate-800: oklch(27.9% 0.041 260.031); --color-neutral-100: oklch(97% 0 0); --color-neutral-200: oklch(92.2% 0 0); --color-neutral-300: oklch(87% 0 0); @@ -197,18 +199,54 @@ .relative { position: relative; } + .sticky { + position: sticky; + } + .top-0 { + top: calc(var(--spacing) * 0); + } + .top-1 { + top: calc(var(--spacing) * 1); + } + .top-1\/2 { + top: calc(1/2 * 100%); + } + .top-2 { + top: calc(var(--spacing) * 2); + } .top-2\.5 { top: calc(var(--spacing) * 2.5); } + .top-25 { + top: calc(var(--spacing) * 25); + } + .top-26 { + top: calc(var(--spacing) * 26); + } .top-\[0\.125rem\] { top: 0.125rem; } .right-1 { right: calc(var(--spacing) * 1); } + .right-2 { + right: calc(var(--spacing) * 2); + } .right-2\.5 { right: calc(var(--spacing) * 2.5); } + .left-1 { + left: calc(var(--spacing) * 1); + } + .left-1\/2 { + left: calc(1/2 * 100%); + } + .z-10 { + z-index: 10; + } + .z-100 { + z-index: 100; + } .col-span-2 { grid-column: span 2 / span 2; } @@ -236,6 +274,9 @@ .ml-1 { margin-left: calc(var(--spacing) * 1); } + .ml-2 { + margin-left: calc(var(--spacing) * 2); + } .icon-\[material-symbols-light--cancel-outline\] { display: inline-block; width: 1.25em; @@ -342,6 +383,9 @@ .inline { display: inline; } + .inline-flex { + display: inline-flex; + } .table { display: table; } @@ -360,6 +404,12 @@ .h-2 { height: calc(var(--spacing) * 2); } + .h-3 { + height: calc(var(--spacing) * 3); + } + .h-3\.5 { + height: calc(var(--spacing) * 3.5); + } .h-4 { height: calc(var(--spacing) * 4); } @@ -369,6 +419,9 @@ .h-8 { height: calc(var(--spacing) * 8); } + .h-10 { + height: calc(var(--spacing) * 10); + } .h-\[100vh\] { height: 100vh; } @@ -378,12 +431,21 @@ .w-2 { width: calc(var(--spacing) * 2); } + .w-3 { + width: calc(var(--spacing) * 3); + } + .w-3\.5 { + width: calc(var(--spacing) * 3.5); + } .w-4 { width: calc(var(--spacing) * 4); } .w-5 { width: calc(var(--spacing) * 5); } + .w-9 { + width: calc(var(--spacing) * 9); + } .w-9\/10 { width: calc(9/10 * 100%); } @@ -396,6 +458,9 @@ .w-full { width: 100%; } + .flex-shrink { + flex-shrink: 1; + } .flex-shrink-0 { flex-shrink: 0; } @@ -411,9 +476,34 @@ .basis-\[content\] { flex-basis: content; } + .border-collapse { + border-collapse: collapse; + } + .-translate-x-1 { + --tw-translate-x: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .-translate-x-1\/2 { + --tw-translate-x: calc(calc(1/2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .-translate-y-1 { + --tw-translate-y: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .-translate-y-1\/2 { + --tw-translate-y: calc(calc(1/2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } .cursor-pointer { cursor: pointer; } + .resize { + resize: both; + } .scroll-m-2 { scroll-margin: calc(var(--spacing) * 2); } @@ -547,6 +637,9 @@ .border-neutral-600 { border-color: var(--color-neutral-600); } + .border-slate-800 { + border-color: var(--color-slate-800); + } .bg-accent { background-color: var(--color-accent); } @@ -568,6 +661,9 @@ .bg-red-600 { background-color: var(--color-red-600); } + .mask-repeat { + mask-repeat: repeat; + } .p-1 { padding: calc(var(--spacing) * 1); } @@ -632,12 +728,28 @@ .text-red-600 { color: var(--color-red-600); } + .text-slate-600 { + color: var(--color-slate-600); + } .text-slate-700 { color: var(--color-slate-700); } + .text-white { + color: var(--color-white); + } .uppercase { text-transform: uppercase; } + .underline { + text-decoration-line: underline; + } + .opacity-0 { + opacity: 0%; + } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } .filter { filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } @@ -646,6 +758,11 @@ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } .transition-colors { transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); @@ -655,6 +772,10 @@ --tw-duration: 300ms; transition-duration: 300ms; } + .select-none { + -webkit-user-select: none; + user-select: none; + } .\*\:text-center { :is(& > *) { text-align: center; @@ -711,11 +832,26 @@ display: none; } } + .peer-checked\:opacity-100 { + &:is(:where(.peer):checked ~ *) { + opacity: 100%; + } + } .placeholder\:text-neutral-400 { &::placeholder { color: var(--color-neutral-400); } } + .checked\:border-slate-800 { + &:checked { + border-color: var(--color-slate-800); + } + } + .checked\:bg-slate-800 { + &:checked { + background-color: var(--color-slate-800); + } + } .hover\:border-neutral-500 { &:hover { @media (hover: hover) { @@ -798,12 +934,6 @@ } } } - .max-md\:border-b-1 { - @media (width < 48rem) { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 1px; - } - } .max-md\:bg-neutral-300 { @media (width < 48rem) { background-color: var(--color-neutral-300); @@ -819,11 +949,6 @@ grid-column: span 3 / span 3; } } - .md\:col-span-4 { - @media (width >= 48rem) { - grid-column: span 4 / span 4; - } - } .md\:mx-\[10\%\] { @media (width >= 48rem) { margin-inline: 10%; @@ -849,6 +974,11 @@ width: calc(1/2 * 100%); } } + .md\:flex-row { + @media (width >= 48rem) { + flex-direction: row; + } + } .md\:px-4 { @media (width >= 48rem) { padding-inline: calc(var(--spacing) * 4); @@ -1011,6 +1141,41 @@ } } } +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} @property --tw-divide-x-reverse { syntax: "*"; inherits: false; @@ -1030,6 +1195,11 @@ syntax: "*"; inherits: false; } +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} @property --tw-blur { syntax: "*"; inherits: false; @@ -1090,10 +1260,19 @@ @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; --tw-divide-x-reverse: 0; --tw-border-style: solid; --tw-divide-y-reverse: 0; --tw-font-weight: initial; + --tw-outline-style: solid; --tw-blur: initial; --tw-brightness: initial; --tw-contrast: initial; diff --git a/Backend/static/logo.png b/Backend/static/logo.png new file mode 100644 index 0000000..53694f9 Binary files /dev/null and b/Backend/static/logo.png differ diff --git a/Backend/static/script.js b/Backend/static/script.js index 2f3b643..c54487d 100644 --- a/Backend/static/script.js +++ b/Backend/static/script.js @@ -15,7 +15,9 @@ function editWorkday(element, event, id, isWorkDay) { event.preventDefault(); let form = document.getElementById(id); if (form == null) { - form = element.closest(".grid-sub").querySelector(".all-booking-component > form"); + form = element + .closest(".grid-sub") + .querySelector(".all-booking-component > form"); } clearEditState(); @@ -37,10 +39,21 @@ function editWorkday(element, event, id, isWorkDay) { const absenceForm = document.getElementById("absence_form"); if (id == 0) { - absenceForm.querySelector("[name=date_from]").value = form.id.replace("time-", ""); - absenceForm.querySelector("[name=date_to]").value = form.id.replace("time-", ""); + absenceForm.querySelector("[name=date_from]").value = form.id.replace( + "time-", + "", + ); + absenceForm.querySelector("[name=date_to]").value = form.id.replace( + "time-", + "", + ); } else { - syncFields(form, absenceForm, ["date_from", "date_to", "aw_type", "aw_id"]); + syncFields(form, absenceForm, [ + "date_from", + "date_to", + "aw_type", + "aw_id", + ]); } } } @@ -49,11 +62,6 @@ function toggleAbsenceEdit(state) { const form = document.getElementById("absence_form"); if (state) { form.classList.remove("hidden"); - form.scrollIntoView({ - behavior: "smooth", - block: "start", - inline: "nearest", - }); } else { form.classList.add("hidden"); } @@ -79,3 +87,18 @@ function navigateWeek(element, event, direction) { function logoutUser() { fetch("/user/logout", {}).then(() => globalThis.location.reload()); } + +function checkAll(pattern, state) { + for (let input of document.querySelectorAll(`input[id^=${pattern}]`)) { + input.checked = state; + } +} + +bookingForms = document.querySelectorAll("form.bookings"); +for (form of bookingForms) { + let selectKommenInput = form.querySelector("input[name='select_kommen']"); + let kommenGehenSelector = form.querySelector("select"); + if (selectKommenInput) { + kommenGehenSelector.value = selectKommenInput.value == "true" ? 3 : 4; + } +} diff --git a/Backend/template.typ b/Backend/template.typ new file mode 100644 index 0000000..ade04ab --- /dev/null +++ b/Backend/template.typ @@ -0,0 +1,92 @@ +#let table-header(..headers) = { + table.header( + ..headers.pos().map(h => strong(h)) + ) +} + + +#let abrechnung(meta, days) = { + set page(paper: "a4", margin: (x:1.5cm, y:2.25cm), + footer:[#grid( + columns: (3fr, .65fr), + align: left + horizon, + inset: .5em, + [#meta.EmployeeName -- #meta.TimeRange], grid.cell(rowspan: 2)[#image("static/logo.png")], + [Arbeitszeitrechnung maschinell erstellt am #meta.CurrentTimestamp], + ) + ]) + set text(font: "Noto Sans", size:10pt, fill: luma(10%)) + set table( + stroke: 0.5pt + luma(10%), + inset: .5em, + align: center + horizon, + ) + show text: it => { + if it.text == "0min"{ + text(oklch(70.8%, 0, 0deg))[#it] + }else if it.text.starts-with("-"){ + text(red)[#it] + }else{ + it + } + } + + + [= Abrechnung Arbeitszeit -- #meta.EmployeeName] + + [Zeitraum: #meta.TimeRange] + + table( + columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1.25fr), + fill: (x, y) => + if y == 0 { oklch(87%, 0, 0deg) }, + table-header( + [Datum], [Kommen], [Gehen], [Arbeitsart], [Stunden], [Pause], [Überstunden] + ), + .. for day in days { + ( + [#day.Date], + if day.DayParts.len() == 0{ + table.cell(colspan: 3)[Keine Buchungen] + }else if not day.DayParts.first().IsWorkDay{ + table.cell(colspan: 3)[#day.DayParts.first().WorkType] + } + else { + + table.cell(colspan: 3, inset: 0em)[ + + #table( + columns: (1fr, 1fr, 1fr), + .. for Zeit in day.DayParts { + ( + [#Zeit.BookingFrom], + [#Zeit.BookingTo], + [#Zeit.WorkType], + ) + }, + ) + ] + }, + [#day.Worktime], + [#day.Pausetime], + [#day.Overtime], + ) + if day.IsFriday { + ( table.cell(colspan: 7, fill: oklch(87%, 0, 0deg))[Wochenende], ) // note the trailing comma + } + } + ) + + table( + columns: (3fr, 1fr), + align: right, + inset: (x: .25em, y:.75em), + stroke: none, + table.hline(start: 0, end: 2, stroke: stroke(dash:"dashed", thickness:.5pt)), + [Arbeitszeit :], table.cell(align: left)[#meta.WorkTime], + [Überstunden :], table.cell(align: left)[#meta.Overtime], + [Überstunden :],table.cell(align: left)[#meta.OvertimeTotal], + table.hline(start: 0, end: 2), + +) +} diff --git a/Backend/templates/headerComponent.templ b/Backend/templates/headerComponent.templ index ec2d673..abcf693 100644 --- a/Backend/templates/headerComponent.templ +++ b/Backend/templates/headerComponent.templ @@ -1,11 +1,13 @@ package templates templ headerComponent() { + // {{ user := ctx.Value("user").(models.User) }}
Zeitverwaltung Abrechnung if true { - Anwesenheit + Monatsabrechnung + Anwesenheit } Einstellungen @LogoutButton() diff --git a/Backend/templates/headerComponent_templ.go b/Backend/templates/headerComponent_templ.go index aeef6ec..53d8123 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.924 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -34,7 +34,7 @@ func headerComponent() templ.Component { return templ_7745c5c3_Err } if true { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "Anwesenheit ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "Monatsabrechnung Anwesenheit ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/pages.templ b/Backend/templates/pages.templ index b2a3475..55735f8 100644 --- a/Backend/templates/pages.templ +++ b/Backend/templates/pages.templ @@ -29,10 +29,8 @@ templ LoginPage(success bool, errorMsg string) {
} -templ UserPage(status int) { - {{ - user := ctx.Value("user").(models.User) - }} +templ SettingsPage(status int) { + {{ user := ctx.Value("user").(models.User) }} @Base() @headerComponent()
@@ -60,6 +58,8 @@ templ UserPage(status int) {

Nutzername: { user.Vorname } { user.Name }

Personalnummer: { user.PersonalNummer }

+

Arbeitszeit pro Tag: { helper.FormatDuration(user.ArbeitszeitProTag()) }

+

Arbeitszeit pro Woche: { helper.FormatDuration(user.ArbeitszeitProWoche()) }

@@ -83,60 +83,6 @@ templ statusCheckMark(status models.WeekStatus, target models.WeekStatus) { } } -templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) { - @Base() - @headerComponent() -
-
-
-

Eigene Abrechnung

-
-
- @workWeekComponent(userWeek, false) - if len(weeks) > 0 { -
-

Abrechnung Mitarbeiter

-
- } - for _, week := range weeks { - @workWeekComponent(week, true) - } -
-} - -templ TeamPresencePage(teamPresence map[models.User]bool) { - @Base() - @headerComponent() -
-
-

Mitarbeiter

-
- for user, present := range teamPresence { -
-
- @timeGaugeComponent(helper.BoolToInt8(present)*100-1, false) -

{ user.Vorname } { user.Name }

-
-
- if present { - Anwesend - } else { - Abwesend - } -
-
- } - //
- //

Nicht Anwesend

- //
- // for _, user := range teamPresence[false] { - // @userPresenceComponent(user, false) - // } - //
- //
-
-} - templ LogoutButton() { } diff --git a/Backend/templates/pages_templ.go b/Backend/templates/pages_templ.go index 491784c..0e5daff 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.924 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -96,7 +96,7 @@ func LoginPage(success bool, errorMsg string) templ.Component { }) } -func UserPage(status int) templ.Component { +func SettingsPage(status int) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -117,7 +117,6 @@ func UserPage(status int) templ.Component { templ_7745c5c3_Var4 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - user := ctx.Value("user").(models.User) templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { @@ -155,7 +154,7 @@ func UserPage(status int) templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 61, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 59, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -168,7 +167,7 @@ func UserPage(status int) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 61, Col: 78} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 59, Col: 78} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -181,13 +180,39 @@ func UserPage(status int) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(user.PersonalNummer) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 62, Col: 75} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 60, Col: 75} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

Nutzer abmelden

Nutzer von Weboberfläche abmelden.

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

Arbeitszeit pro Tag: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(user.ArbeitszeitProTag())) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 61, Col: 108} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

Arbeitszeit pro Woche: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(user.ArbeitszeitProWoche())) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 62, Col: 112} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

Nutzer abmelden

Nutzer von Weboberfläche abmelden.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -196,99 +221,6 @@ 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_Var8 := templ.GetChildren(ctx) - if templ_7745c5c3_Var8 == nil { - templ_7745c5c3_Var8 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - if status >= target { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") - 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 - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var9 := templ.GetChildren(ctx) - if templ_7745c5c3_Var9 == nil { - templ_7745c5c3_Var9 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

Eigene Abrechnung

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = workWeekComponent(userWeek, false).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if len(weeks) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

Abrechnung Mitarbeiter

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - for _, week := range weeks { - templ_7745c5c3_Err = workWeekComponent(week, true).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func TeamPresencePage(teamPresence map[models.User]bool) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -309,76 +241,16 @@ func TeamPresencePage(teamPresence map[models.User]bool) templ.Component { templ_7745c5c3_Var10 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

Mitarbeiter

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for user, present := range teamPresence { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + if status >= target { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = timeGaugeComponent(helper.BoolToInt8(present)*100-1, false).Render(ctx, templ_7745c5c3_Buffer) + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 118, Col: 22} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 118, Col: 36} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if present { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Anwesend") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Abwesend") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err } return nil }) @@ -400,12 +272,12 @@ func LogoutButton() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var13 := templ.GetChildren(ctx) - if templ_7745c5c3_Var13 == nil { - templ_7745c5c3_Var13 = templ.NopComponent + templ_7745c5c3_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/pdf.templ b/Backend/templates/pdf.templ index 204e9dd..ab297a1 100644 --- a/Backend/templates/pdf.templ +++ b/Backend/templates/pdf.templ @@ -3,75 +3,132 @@ package templates import ( "arbeitszeitmessung/helper" "arbeitszeitmessung/models" + "fmt" "time" ) -templ PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) { - {{ - _, kw := tsStart.ISOWeek() - noBorder := "" - }} +templ PDFForm(teamMembers []models.User) { @Base() - -
-

{ e.Vorname } { e.Name }

-

Zeitraum: { tsStart.Format("02.01.2006") } - { tsEnd.Format("02.01.2006") }

-

Arbeitszeit: { helper.FormatDuration(worktime) }

-

Überstunden: { helper.FormatDuration(overtime) }

+ @headerComponent() +
+
+

Monatsabrechnung erstellen

-
-

{ kw }

-

Kommen

-

Gehen

-

Arbeitsart

-

Stunden

-

Pause

-

Überstunden

- for index, day := range workDays { - {{ - if index == len(workDays)-1 { - noBorder = "border-b-0" - } - }} -

{ day.Date().Format("02.01.2006") }

-
- if day.IsWorkDay() { - {{ - workDay, _ := day.(*models.WorkDay) - }} - for bookingI := 0; bookingI < len(workDay.Bookings); bookingI+= 2 { -

{ workDay.Bookings[bookingI].Timestamp.Format("15:04") }

-

{ workDay.Bookings[bookingI+1].Timestamp.Format("15:04") }

-

{ workDay.Bookings[bookingI].BookingType.Name }

- } - if workDay.IsKurzArbeit() { -

Kurzarbeit

- } - } else { - {{ - absentDay, _ := day.(*models.Absence) - }} -

{ absentDay.AbwesenheitTyp.Name }

- } +
+
Zeitraum wählen
+
+ + +
+
+
+
+
Mitarbeiter wählen
+
+
+ +
- {{ work, pause, overtime := day.GetAllWorkTimesVirtual(e) }} - @ColorDuration(work, noBorder) - @ColorDuration(pause, noBorder) - @ColorDuration(overtime, noBorder+" border-r-0") - if day.Date().Weekday() == time.Friday { -

Wochenende

+ for _, member := range teamMembers { + @CheckboxComponent(member.PersonalNummer, fmt.Sprintf("%s %s", member.Vorname, member.Name)) } - } +
+
- +
+
PDFs Bündeln
+
+ + +
+
+ } +templ CheckboxComponent(pNr int, label string) { + {{ id := fmt.Sprintf("pdf-%d", pNr) }} +
+ +
+} + +// templ PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) { +// {{ +// _, kw := tsStart.ISOWeek() +// noBorder := "" +// }} +// @Base() +// +//
+//

{ e.Vorname } { e.Name }

+//

Zeitraum: { tsStart.Format("02.01.2006") } - { tsEnd.Format("02.01.2006") }

+//

Arbeitszeit: { helper.FormatDuration(worktime) }

+//

Überstunden: { helper.FormatDuration(overtime) }

+//
+//
+//

{ kw }

+//

Kommen

+//

Gehen

+//

Arbeitsart

+//

Stunden

+//

Pause

+//

Überstunden

+// for index, day := range workDays { +// {{ +// if index == len(workDays)-1 { +// noBorder = "border-b-0" +// } +// }} +//

{ day.Date().Format("02.01.2006") }

+//
+// if day.IsWorkDay() { +// {{ +// workDay, _ := day.(*models.WorkDay) +// }} +// for bookingI := 0; bookingI < len(workDay.Bookings); bookingI+= 2 { +//

{ workDay.Bookings[bookingI].Timestamp.Format("15:04") }

+//

{ workDay.Bookings[bookingI+1].Timestamp.Format("15:04") }

+//

{ workDay.Bookings[bookingI].BookingType.Name }

+// } +// if workDay.IsKurzArbeit() { +// {{ +// timeFrom, timeTo := workDay.GenerateKurzArbeitBookings(e) +// }} +//

{ timeFrom.Format("15:04") }

+//

{ timeTo.Format("15:04") }

+//

Kurzarbeit

+// } +// } else { +// {{ +// absentDay, _ := day.(*models.Absence) +// }} +//

{ absentDay.AbwesenheitTyp.Name }

+// } +//
+// {{ work, pause, overtime := day.GetTimesVirtual(e) }} +// @ColorDuration(work, noBorder) +// @ColorDuration(pause, noBorder) +// @ColorDuration(overtime, noBorder+" border-r-0") +// if day.Date().Weekday() == time.Friday { +//

Wochenende

+// } +// } +//
+//
+// } templ ColorDuration(d time.Duration, classes string) { {{ - color := "" - if d.Abs() < time.Minute { - color = "text-neutral-300" - } + color := "" + if d.Abs() < time.Minute { + color = "text-neutral-300" + } }}

{ helper.FormatDurationFill(d, true) }

} diff --git a/Backend/templates/pdf_templ.go b/Backend/templates/pdf_templ.go index 3c73ded..312984f 100644 --- a/Backend/templates/pdf_templ.go +++ b/Backend/templates/pdf_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.924 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -11,10 +11,11 @@ import templruntime "github.com/a-h/templ/runtime" import ( "arbeitszeitmessung/helper" "arbeitszeitmessung/models" + "fmt" "time" ) -func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) templ.Component { +func PDFForm(teamMembers []models.User) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -35,286 +36,72 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - - _, kw := tsStart.ISOWeek() - noBorder := "" templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

") + templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Monatsabrechnung erstellen

Zeitraum wählen
Mitarbeiter wählen
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var3 string - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 17, Col: 56} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("true"))) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

Zeitraum: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var5 string - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(tsEnd.Format("02.01.2006")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 18, Col: 98} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("checkAll", "pdf-", templ.JSExpression("false"))) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

Arbeitszeit: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(overtime)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 20, Col: 59} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(kw) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 23, Col: 52} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

Kommen

Gehen

Arbeitsart

Stunden

Pause

Überstunden

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for index, day := range workDays { - - if index == len(workDays)-1 { - noBorder = "border-b-0" - } - var templ_7745c5c3_Var9 = []any{noBorder} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...) + for _, member := range teamMembers { + templ_7745c5c3_Err = CheckboxComponent(member.PersonalNummer, fmt.Sprintf("%s %s", member.Vorname, member.Name)).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 36, Col: 59} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var12 = []any{"grid grid-cols-subgrid col-span-3 " + noBorder} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if day.IsWorkDay() { - - workDay, _ := day.(*models.WorkDay) - for bookingI := 0; bookingI < len(workDay.Bookings); bookingI += 2 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI].Timestamp.Format("15:04")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 43, Col: 64} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI+1].Timestamp.Format("15:04")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 44, Col: 66} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI].BookingType.Name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 45, Col: 55} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if workDay.IsKurzArbeit() { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

Kurzarbeit

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - } else { - - absentDay, _ := day.(*models.Absence) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(absentDay.AbwesenheitTyp.Name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 54, Col: 62} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - work, pause, overtime := day.GetAllWorkTimesVirtual(e) - templ_7745c5c3_Err = ColorDuration(work, noBorder).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = ColorDuration(pause, noBorder).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = ColorDuration(overtime, noBorder+" border-r-0").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if day.Date().Weekday() == time.Friday { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

Wochenende

") - 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, 7, "
PDFs Bündeln
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -322,6 +109,165 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays }) } +func CheckboxComponent(pNr int, label string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + id := fmt.Sprintf("pdf-%d", pNr) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// templ PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) { +// {{ +// _, kw := tsStart.ISOWeek() +// noBorder := "" +// }} +// @Base() +// +//
+//

{ e.Vorname } { e.Name }

+//

Zeitraum: { tsStart.Format("02.01.2006") } - { tsEnd.Format("02.01.2006") }

+//

Arbeitszeit: { helper.FormatDuration(worktime) }

+//

Überstunden: { helper.FormatDuration(overtime) }

+//
+//
+//

{ kw }

+//

Kommen

+//

Gehen

+//

Arbeitsart

+//

Stunden

+//

Pause

+//

Überstunden

+// for index, day := range workDays { +// {{ +// if index == len(workDays)-1 { +// noBorder = "border-b-0" +// } +// }} +//

{ day.Date().Format("02.01.2006") }

+//
+// if day.IsWorkDay() { +// {{ +// workDay, _ := day.(*models.WorkDay) +// }} +// for bookingI := 0; bookingI < len(workDay.Bookings); bookingI+= 2 { +//

{ workDay.Bookings[bookingI].Timestamp.Format("15:04") }

+//

{ workDay.Bookings[bookingI+1].Timestamp.Format("15:04") }

+//

{ workDay.Bookings[bookingI].BookingType.Name }

+// } +// if workDay.IsKurzArbeit() { +// {{ +// timeFrom, timeTo := workDay.GenerateKurzArbeitBookings(e) +// }} +//

{ timeFrom.Format("15:04") }

+//

{ timeTo.Format("15:04") }

+//

Kurzarbeit

+// } +// } else { +// {{ +// absentDay, _ := day.(*models.Absence) +// }} +//

{ absentDay.AbwesenheitTyp.Name }

+// } +//
+// {{ work, pause, overtime := day.GetTimesVirtual(e) }} +// @ColorDuration(work, noBorder) +// @ColorDuration(pause, noBorder) +// @ColorDuration(overtime, noBorder+" border-r-0") +// if day.Date().Weekday() == time.Friday { +//

Wochenende

+// } +// } +//
+//
+// } func ColorDuration(d time.Duration, classes string) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context @@ -338,48 +284,47 @@ func ColorDuration(d time.Duration, classes string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var18 := templ.GetChildren(ctx) - if templ_7745c5c3_Var18 == nil { - templ_7745c5c3_Var18 = templ.NopComponent + templ_7745c5c3_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - color := "" if d.Abs() < time.Minute { color = "text-neutral-300" } - var templ_7745c5c3_Var19 = []any{color + " " + classes} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...) + var templ_7745c5c3_Var12 = []any{color + " " + classes} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDurationFill(d, true)) + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDurationFill(d, true)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 76, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 133, Col: 72} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/presencePage.templ b/Backend/templates/presencePage.templ new file mode 100644 index 0000000..f1ca3d1 --- /dev/null +++ b/Backend/templates/presencePage.templ @@ -0,0 +1,29 @@ +package templates + +import "arbeitszeitmessung/models" +import "arbeitszeitmessung/helper" + +templ TeamPresencePage(teamPresence map[models.User]bool) { + @Base() + @headerComponent() +
+
+

Mitarbeiter

+
+ for user, present := range teamPresence { +
+
+ @timeGaugeComponent(helper.BoolToInt8(present)*100-1, false) +

{ user.Vorname } { user.Name }

+
+
+ if present { + Anwesend + } else { + Abwesend + } +
+
+ } +
+} diff --git a/Backend/templates/presencePage_templ.go b/Backend/templates/presencePage_templ.go new file mode 100644 index 0000000..7655580 --- /dev/null +++ b/Backend/templates/presencePage_templ.go @@ -0,0 +1,110 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.960 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "arbeitszeitmessung/models" +import "arbeitszeitmessung/helper" + +func TeamPresencePage(teamPresence map[models.User]bool) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Mitarbeiter

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for user, present := range teamPresence { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = timeGaugeComponent(helper.BoolToInt8(present)*100-1, false).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/presencePage.templ`, Line: 17, Col: 22} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/presencePage.templ`, Line: 17, Col: 36} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if present { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Anwesend") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "Abwesend") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/Backend/templates/reportPage.templ b/Backend/templates/reportPage.templ new file mode 100644 index 0000000..bc74e34 --- /dev/null +++ b/Backend/templates/reportPage.templ @@ -0,0 +1,146 @@ +package templates + +import ( + "arbeitszeitmessung/helper" + "arbeitszeitmessung/models" + "fmt" + "strconv" + "time" +) + +templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) { + @Base() + @headerComponent() +
+
+
+

Eigene Abrechnung

+
+
+ @workWeekComponent(userWeek, false) + if len(weeks) > 0 { +
+

Abrechnung Mitarbeiter

+
+ } + for _, week := range weeks { + @workWeekComponent(week, true) + } +
+} + +templ workWeekComponent(week models.WorkWeek, onlyAccept bool) { + {{ + year, kw := week.WeekStart.ISOWeek() + progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100 + }} +
+
+ if !onlyAccept { +
+ @weekPicker(week.WeekStart) +
+ } +

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

+
+ if !onlyAccept { +
+ + @statusCheckMark(week.CheckStatus(), models.WeekStatusSent) + Gesendet + + + @statusCheckMark(week.CheckStatus(), models.WeekStatusAccepted) + Akzeptiert + +
+ } +
+ @timeGaugeComponent(int8(progress), false) +
+

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

+

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

+
+
+
+
+
+ for _, day := range week.Days { + @defaultWeekDayComponent(week.User, day) + } +
+
+ if onlyAccept { +

Woche: { fmt.Sprintf("%02d-%d", kw, year) }

+ } else { +
+ @weekPicker(week.WeekStart) +
+ } +
+ {{ + week.CheckStatus() + method := "accept" + if !onlyAccept { + method = "send" + } + }} + + + + if onlyAccept { + if week.Status == models.WeekStatusDifferences { +

Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen

+ } + + } else { + switch { + case week.RequiresAction(): +

bitte zuerst Buchungen anpassen

+ case time.Since(week.WeekStart) < 24*7*time.Hour: +

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

+ case week.Status == models.WeekStatusNone: +

an Vorgesetzten senden

+ case week.Status == models.WeekStatusSent: +

an Vorgesetzten gesendet

+ case week.Status == models.WeekStatusAccepted: +

vom Vorgesetzten bestätigt

+ } + + + } +
+
+
+} + +templ defaultWeekDayComponent(u models.User, day models.IWorkDay) { +
+ @timeGaugeComponent(day.GetDayProgress(u), false) +
+

{ day.Date().Format("02.01.2006") }

+ {{ work, pause, _ := day.GetTimes(u, models.WorktimeBaseDay, false) }} + if day.IsWorkDay() || day.GetDayProgress(u) < 100 { +
+ { helper.FormatDuration(work) } + { helper.FormatDuration(pause) } +
+ } + @weekDayTypeSwitcher(day) +
+
+} + +templ weekDayTypeSwitcher(day models.IWorkDay) { + switch day.Type() { + case models.DayTypeWorkday: + {{ workDay, _ := day.(*models.WorkDay) }} + @workDayWeekComponent(workDay) + case models.DayTypeCompound: + for _, c := range day.(*models.CompoundDay).DayParts { + @weekDayTypeSwitcher(c) + } + default: +
{ day.ToString() }
+ } +} diff --git a/Backend/templates/reportPage_templ.go b/Backend/templates/reportPage_templ.go new file mode 100644 index 0000000..96ed8f5 --- /dev/null +++ b/Backend/templates/reportPage_templ.go @@ -0,0 +1,545 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.960 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "arbeitszeitmessung/helper" + "arbeitszeitmessung/models" + "fmt" + "strconv" + "time" +) + +func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = headerComponent().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Eigene Abrechnung

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = workWeekComponent(userWeek, false).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(weeks) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

Abrechnung Mitarbeiter

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for _, week := range weeks { + templ_7745c5c3_Err = workWeekComponent(week, true).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + year, kw := week.WeekStart.ISOWeek() + progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !onlyAccept { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = weekPicker(week.WeekStart).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + 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 + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Vorname) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 44, Col: 53} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 44, Col: 72} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !onlyAccept { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = statusCheckMark(week.CheckStatus(), models.WeekStatusSent).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "Gesendet ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = statusCheckMark(week.CheckStatus(), models.WeekStatusAccepted).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Akzeptiert
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = timeGaugeComponent(int8(progress), false).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

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

Überstunden: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 62, Col: 90} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, day := range week.Days { + templ_7745c5c3_Err = defaultWeekDayComponent(week.User, day).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if onlyAccept { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "

Woche: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d-%d", kw, year)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 74, Col: 86} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = weekPicker(week.WeekStart).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + week.CheckStatus() + method := "accept" + if !onlyAccept { + method = "send" + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if onlyAccept { + if week.Status == models.WeekStatusDifferences { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "

Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + switch { + case week.RequiresAction(): + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "

bitte zuerst Buchungen anpassen

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case time.Since(week.WeekStart) < 24*7*time.Hour: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "

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

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case week.Status == models.WeekStatusNone: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "

an Vorgesetzten senden

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case week.Status == models.WeekStatusSent: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

an Vorgesetzten gesendet

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case week.Status == models.WeekStatusAccepted: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "

vom Vorgesetzten bestätigt

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " Korrigieren = models.WeekStatusSent || week.RequiresAction() { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, " disabled") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " type=\"submit\" class=\"btn\">Senden") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = timeGaugeComponent(day.GetDayProgress(u), false).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 121, Col: 152} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + work, pause, _ := day.GetTimes(u, models.WorktimeBaseDay, false) + if day.IsWorkDay() || day.GetDayProgress(u) < 100 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(work)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 125, Col: 60} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(pause)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 126, Col: 66} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = weekDayTypeSwitcher(day).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func weekDayTypeSwitcher(day models.IWorkDay) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var16 := templ.GetChildren(ctx) + if templ_7745c5c3_Var16 == nil { + templ_7745c5c3_Var16 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + switch day.Type() { + case models.DayTypeWorkday: + workDay, _ := day.(*models.WorkDay) + templ_7745c5c3_Err = workDayWeekComponent(workDay).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case models.DayTypeCompound: + for _, c := range day.(*models.CompoundDay).DayParts { + templ_7745c5c3_Err = weekDayTypeSwitcher(c).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(day.ToString()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reportPage.templ`, Line: 144, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/Backend/templates/teamComponents.templ b/Backend/templates/teamComponents.templ index e5738b9..fd09824 100644 --- a/Backend/templates/teamComponents.templ +++ b/Backend/templates/teamComponents.templ @@ -1,17 +1,13 @@ package templates import ( - "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "fmt" - "strconv" "time" ) templ weekPicker(weekStart time.Time) { - {{ - year, kw := weekStart.ISOWeek() - }} + {{ year, kw := weekStart.ISOWeek() }}
- } else { - switch { - case week.RequiresAction(): -

bitte zuerst Buchungen anpassen

- case time.Since(week.WeekStart) < 24*7*time.Hour: -

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

- case week.Status == models.WeekStatusNone: -

an Vorgesetzten senden

- case week.Status == models.WeekStatusSent: -

an Vorgesetzten gesendet

- case week.Status == models.WeekStatusAccepted: -

vom Vorgesetzten bestätigt

- } - - - } - - - + } else { +

Bitte anpassen

+ } } templ userPresenceComponent(user models.User, present bool) { diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index ec539f2..0078d43 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.924 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -9,10 +9,8 @@ import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" import ( - "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "fmt" - "strconv" "time" ) @@ -37,7 +35,6 @@ func weekPicker(weekStart time.Time) 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 = timeGaugeComponent(day.GetDayProgress(u), false).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 136} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if day.IsWorkDay() { - - workDay, _ := day.(*models.WorkDay) - work, pause, _ := workDay.GetAllWorkTimesReal(u) - if !workDay.RequiresAction() { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(work)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 43, Col: 61} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(pause)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 44, Col: 67} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - switch { - case !workDay.TimeFrom.Equal(workDay.TimeTo): - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.TimeFrom.Format("15:04")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 50, Col: 48} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " - ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.TimeTo.Format("15:04")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 52, Col: 46} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

Keine Anwesenheit

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

Bitte anpassen

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - } else { - - absentDay, _ := day.(*models.Absence) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") + if !workDay.RequiresAction() { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(absentDay.AbwesenheitTyp.Name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 64, Col: 40} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - 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_Var14 := templ.GetChildren(ctx) - if templ_7745c5c3_Var14 == nil { - templ_7745c5c3_Var14 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !day.RequiresAction() { - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var15 := templ.GetChildren(ctx) - if templ_7745c5c3_Var15 == nil { - templ_7745c5c3_Var15 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - - year, kw := week.WeekStart.ISOWeek() - progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !onlyAccept { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = weekPicker(week.WeekStart).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Vorname) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 93, Col: 53} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 93, Col: 72} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !onlyAccept { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = statusCheckMark(week.CheckStatus(), models.WeekStatusSent).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "Gesendet ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = statusCheckMark(week.CheckStatus(), models.WeekStatusAccepted).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Akzeptiert
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = timeGaugeComponent(int8(progress), false).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

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

Überstunden: ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true))) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 111, Col: 90} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, day := range week.Days { - templ_7745c5c3_Err = defaultWeekDayComponent(week.User, day).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if onlyAccept { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "

Woche: ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d-%d", kw, year)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 123, Col: 86} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = weekPicker(week.WeekStart).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - - week.CheckStatus() - method := "accept" - if !onlyAccept { - method = "send" - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if onlyAccept { - if week.Status == models.WeekStatusDifferences { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "

Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { switch { - case week.RequiresAction(): - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "

bitte zuerst Buchungen anpassen

") + case !workDay.TimeFrom.Equal(workDay.TimeTo): + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - case time.Since(week.WeekStart) < 24*7*time.Hour: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "

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

") + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.TimeFrom.Format("15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 33, Col: 45} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - case week.Status == models.WeekStatusNone: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "

an Vorgesetzten senden

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
- ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - case week.Status == models.WeekStatusSent: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "

an Vorgesetzten gesendet

") + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.TimeTo.Format("15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 43} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - case week.Status == models.WeekStatusAccepted: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "

vom Vorgesetzten bestätigt

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

Keine Anwesenheit

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if week.Status < models.WeekStatusSent { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " disabled") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " type=\"submit\" class=\"btn\">Korrigieren Bitte anpassen

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if time.Since(week.WeekStart) < 24*7*time.Hour || week.Status >= models.WeekStatusSent || week.RequiresAction() { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " disabled") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " type=\"submit\" class=\"btn\">Senden") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err } return nil }) @@ -648,53 +208,53 @@ func userPresenceComponent(user models.User, present bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var24 := templ.GetChildren(ctx) - if templ_7745c5c3_Var24 == nil { - templ_7745c5c3_Var24 = templ.NopComponent + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if present { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
Anwesend
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
Anwesend
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "
Abwesend
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
Abwesend
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname) + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 173, Col: 19} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 52, Col: 19} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) + _, 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, 70, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 173, Col: 33} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 52, Col: 33} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + _, 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, 71, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/timeComponents.templ b/Backend/templates/timeComponents.templ index 799ebda..4641a1e 100644 --- a/Backend/templates/timeComponents.templ +++ b/Backend/templates/timeComponents.templ @@ -20,15 +20,15 @@ templ lineComponent() { } templ changeButtonComponent(id string, workDay bool) { - - + } templ timeGaugeComponent(progress int8, today bool) { @@ -77,32 +77,37 @@ templ absenceComponent(a *models.Absence, isKurzarbeit bool) { } }}
- - - - + @absentInput(a)

- { a.AbwesenheitTyp.Name } + { a.ToString() } if a.IsMultiDay() { bis { a.DateTo.Format("02.01.2006") } }

if isKurzarbeit { - + }
} -templ newBookingComponent(d *models.WorkDay) { - ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -420,7 +374,7 @@ func absenceComponent(a *models.Absence, isKurzarbeit bool) templ.Component { }) } -func newBookingComponent(d *models.WorkDay) templ.Component { +func absentInput(a *models.Absence) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -436,58 +390,133 @@ func newBookingComponent(d *models.WorkDay) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var23 := templ.GetChildren(ctx) - if templ_7745c5c3_Var23 == nil { - templ_7745c5c3_Var23 = templ.NopComponent + templ_7745c5c3_Var19 := templ.GetChildren(ctx) + if templ_7745c5c3_Var19 == nil { + templ_7745c5c3_Var19 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -511,64 +540,74 @@ func bookingComponent(booking models.Booking) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var26 := templ.GetChildren(ctx) - if templ_7745c5c3_Var26 == nil { - templ_7745c5c3_Var26 = templ.NopComponent + templ_7745c5c3_Var28 := templ.GetChildren(ctx) + if templ_7745c5c3_Var28 == nil { + templ_7745c5c3_Var28 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, 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: 119, Col: 91} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var29 string templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(booking.Timestamp.Format("15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 120, Col: 126} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 124, Col: 91} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" class=\"text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer\"> ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" type=\"time\" value=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(booking.Timestamp.Format("15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 125, Col: 126} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" class=\"text-neutral-700 group-[.edit]:inline hidden bg-neutral-100 text-sm px-3 py-2 cursor-pointer\"> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(booking.GetBookingType()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 126, Col: 29} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if booking.IsSubmittedAndChecked() { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

submitted

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -592,12 +631,12 @@ func LegendComponent() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var31 := templ.GetChildren(ctx) - if templ_7745c5c3_Var31 == nil { - templ_7745c5c3_Var31 = 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, 40, "
Fehler
Arbeitszeit unter regulär
Arbeitszeit vollständig
Überstunden
Keine Buchungen
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
Fehler
Arbeitszeit unter regulär
Arbeitszeit vollständig
Überstunden
Keine Buchungen
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/timePage.templ b/Backend/templates/timePage.templ index 7df0b29..3dc9f03 100644 --- a/Backend/templates/timePage.templ +++ b/Backend/templates/timePage.templ @@ -9,9 +9,7 @@ import ( ) templ TimePage(workDays []models.WorkDay, lastSub time.Time) { - {{ - allDays := ctx.Value("days").([]models.IWorkDay) - }} + {{ allDays := ctx.Value("days").([]models.IWorkDay) }} @Base() @headerComponent()
@@ -31,65 +29,67 @@ templ inputForm() { urlParams := ctx.Value("urlParams").(url.Values) user := ctx.Value("user").(models.User) }} -
-
-

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

-
-

Überstunden

-

{ user.Overtime }

+
+
+
+

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

+
+

Überstunden

+

{ user.Overtime }

+
+
+
+ @lineComponent() +
+ + +
+
+
+
-
- @lineComponent() -
- - + + +

Abwesenheit

+
+ +
+ + + + +
+
+
+ + +
+
+ + +
+
+
+ + +
-
- -
- } templ defaultDayComponent(day models.IWorkDay) { {{ user := ctx.Value("user").(models.User) justify := "justify-center" - if day.IsWorkDay() && len(day.(*models.WorkDay).Bookings) > 1 { + if day.IsWorkDay() && !day.IsEmpty() { justify = "justify-between" } }} @@ -98,12 +98,12 @@ templ defaultDayComponent(day models.IWorkDay) { @timeGaugeComponent(day.GetDayProgress(user), day.Date().Equal(time.Now().Truncate(24*time.Hour)))

- { day.Date().Format("02.01.2006") } + { day.Date().Format("02.01.2006") }

if day.IsWorkDay() { {{ - workDay, _ := day.(*models.WorkDay) - work, pause, overtime := workDay.GetAllWorkTimesReal(user) + work, pause, overtime := day.GetTimes(user, models.WorktimeBaseDay, true) + work = day.GetWorktime(user, models.WorktimeBaseDay, false) }} if day.RequiresAction() {

Bitte anpassen

@@ -115,7 +115,7 @@ templ defaultDayComponent(day models.IWorkDay) { if pause > 0 {

{ helper.FormatDuration(pause) }

} - if overtime != 0 && len(workDay.Bookings) > 0 { + if overtime != 0 && day.IsEmpty() == false {

{ helper.FormatDuration(overtime) } @@ -127,40 +127,54 @@ templ defaultDayComponent(day models.IWorkDay) {

@lineComponent() -
- if day.IsWorkDay() { - {{ - workDay, _ := day.(*models.WorkDay) - }} + + if (day.GetDayProgress(user) < 100 || day.IsWorkDay()) { @newAbsenceComponent() - if len(workDay.Bookings) < 1 { -

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

- } - if workDay.IsKurzArbeit() { - @absenceComponent(workDay.GetKurzArbeit(), true) - } - for _, booking := range workDay.Bookings { - @bookingComponent(booking) - } - @newBookingComponent(workDay) + @timeDayTypeSwitch(day, true) + @newBookingComponent(day) } else { - {{ - absentDay, _ := day.(*models.Absence) - }} - @absenceComponent(absentDay, false) + @timeDayTypeSwitch(day, true) }
- @changeButtonComponent("time-"+day.Date().Format("2006-01-02"), day.IsWorkDay()) + @changeButtonComponent("time-"+day.Date().Format(time.DateOnly), true)
} -templ absentInput(a models.Absence) { - - - - +templ timeDayTypeSwitch(day models.IWorkDay, fromCompound bool) { + switch day.Type() { + case models.DayTypeWorkday: + {{ workDay, _ := day.(*models.WorkDay) }} + @workdayComponent(workDay) + case models.DayTypeAbsence: + {{ absentDay, _ := day.(*models.Absence) }} + @absenceComponent(absentDay, fromCompound) + case models.DayTypeCompound: + for _, c := range day.(*models.CompoundDay).DayParts { + @timeDayTypeSwitch(c, true) + } + default: +

{ day.ToString() }

+ } +} + +templ workdayComponent(workDay *models.WorkDay) { + if len(workDay.Bookings) < 1 { +

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

+ } else { + if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 { + @absenceComponent(workDay.GetKurzArbeit(), true) + } + for _, booking := range workDay.Bookings { + @bookingComponent(booking) + } + 0 && workDay.Bookings[len(workDay.Bookings)-1].CheckInOut%2 == 0 }/> + } +} + +templ holidayComponent(d models.IWorkDay) { +

{ d.ToString() }

} diff --git a/Backend/templates/timePage_templ.go b/Backend/templates/timePage_templ.go index d668290..5f55e8d 100644 --- a/Backend/templates/timePage_templ.go +++ b/Backend/templates/timePage_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.924 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -37,7 +37,6 @@ func TimePage(workDays []models.WorkDay, lastSub time.Time) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - allDays := ctx.Value("days").([]models.IWorkDay) templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { @@ -104,17 +103,16 @@ func inputForm() templ.Component { templ_7745c5c3_Var2 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - urlParams := ctx.Value("urlParams").(url.Values) user := ctx.Value("user").(models.User) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname + " " + user.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 36, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 35, Col: 67} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -127,7 +125,7 @@ func inputForm() templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Overtime) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 39, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 38, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -148,7 +146,7 @@ func inputForm() templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(urlParams.Get("time_from")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 45, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 44, Col: 58} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -161,7 +159,7 @@ func inputForm() templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(urlParams.Get("time_to")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 46, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 45, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -179,7 +177,7 @@ func inputForm() templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(int(absence.Id))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 63, Col: 51} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 62, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -192,7 +190,7 @@ func inputForm() templ.Component { var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(absence.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 63, Col: 68} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 62, Col: 69} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -203,7 +201,7 @@ func inputForm() templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -232,10 +230,9 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - user := ctx.Value("user").(models.User) justify := "justify-center" - if day.IsWorkDay() && len(day.(*models.WorkDay).Bookings) > 1 { + if day.IsWorkDay() && !day.IsEmpty() { justify = "justify-between" } var templ_7745c5c3_Var10 = []any{"grid-sub divide-x-1 hover:bg-neutral-200 transition-colors group"} @@ -269,9 +266,9 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("Mon")) + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatGermanDayOfWeek(day.Date())) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 98} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -284,7 +281,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 126} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 142} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -295,9 +292,8 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { return templ_7745c5c3_Err } if day.IsWorkDay() { - - workDay, _ := day.(*models.WorkDay) - work, pause, overtime := workDay.GetAllWorkTimesReal(user) + work, pause, overtime := day.GetTimes(user, models.WorktimeBaseDay, true) + work = day.GetWorktime(user, models.WorktimeBaseDay, false) if day.RequiresAction() { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

Bitte anpassen

") if templ_7745c5c3_Err != nil { @@ -350,7 +346,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if overtime != 0 && len(workDay.Bookings) > 0 { + if overtime != 0 && day.IsEmpty() == false { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -379,7 +375,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var17 = []any{"flex flex-col gap-2 w-full", justify} + var templ_7745c5c3_Var17 = []any{"bookings flex flex-col gap-2 w-full", justify} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -389,9 +385,9 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("time-" + day.Date().Format("2006-01-02")) + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("time-" + day.Date().Format(time.DateOnly)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 130, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 130, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { @@ -414,9 +410,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if day.IsWorkDay() { - - workDay, _ := day.(*models.WorkDay) + if day.GetDayProgress(user) < 100 || day.IsWorkDay() { templ_7745c5c3_Err = newAbsenceComponent().Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -425,53 +419,33 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if len(workDay.Bookings) < 1 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

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

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " ") + templ_7745c5c3_Err = timeDayTypeSwitch(day, true).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if workDay.IsKurzArbeit() { - templ_7745c5c3_Err = absenceComponent(workDay.GetKurzArbeit(), true).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - for _, booking := range workDay.Bookings { - templ_7745c5c3_Err = bookingComponent(booking).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = newBookingComponent(workDay).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = newBookingComponent(day).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - - absentDay, _ := day.(*models.Absence) - templ_7745c5c3_Err = absenceComponent(absentDay, false).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = timeDayTypeSwitch(day, true).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = changeButtonComponent("time-"+day.Date().Format("2006-01-02"), day.IsWorkDay()).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = changeButtonComponent("time-"+day.Date().Format(time.DateOnly), true).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -479,7 +453,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { }) } -func absentInput(a models.Absence) templ.Component { +func timeDayTypeSwitch(day models.IWorkDay, fromCompound bool) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -500,59 +474,145 @@ func absentInput(a models.Absence) templ.Component { templ_7745c5c3_Var20 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(day.ToString()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 160, Col: 22} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +func workdayComponent(workDay *models.WorkDay) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var22 := templ.GetChildren(ctx) + if templ_7745c5c3_Var22 == nil { + templ_7745c5c3_Var22 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if len(workDay.Bookings) < 1 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

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

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 { + templ_7745c5c3_Err = absenceComponent(workDay.GetKurzArbeit(), true).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for _, booking := range workDay.Bookings { + templ_7745c5c3_Err = bookingComponent(booking).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " 0 && workDay.Bookings[len(workDay.Bookings)-1].CheckInOut%2 == 0) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 174, Col: 140} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +func holidayComponent(d models.IWorkDay) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var24 := templ.GetChildren(ctx) + if templ_7745c5c3_Var24 == nil { + templ_7745c5c3_Var24 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateFrom.Format("2006-01-02")) + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(d.ToString()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 162, Col: 78} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 179, Col: 18} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\"> ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/DB/initdb/01_schema.sql b/DB/initdb/01_schema.sql index e3e795f..6e18e78 100755 --- a/DB/initdb/01_schema.sql +++ b/DB/initdb/01_schema.sql @@ -22,7 +22,7 @@ COMMENT ON COLUMN "anwesenheit"."geraet_id" IS 'ID des Lesegerätes'; DROP TABLE IF EXISTS "s_anwesenheit_typen"; CREATE TABLE "s_anwesenheit_typen" ( "anwesenheit_id" int2 PRIMARY KEY, - "anwesenheit_name" varchar(255) + "anwesenheit_name" varchar(255) NOT NULL ); -- ---------------------------- @@ -78,28 +78,45 @@ EXECUTE FUNCTION update_zuletzt_geandert(); DROP TABLE IF EXISTS "wochen_report"; CREATE TABLE "wochen_report" ( "id" serial PRIMARY KEY, - "personal_nummer" int4, - "woche_start" date, + "personal_nummer" int4 NOT NULL, + "woche_start" date NOT NULL, "bestaetigt" bool DEFAULT FALSE, - "arbeitszeit" interval, - "ueberstunden" interval, + "arbeitszeit" interval NOT NULL, + "ueberstunden" interval NOT NULL, + "anwesenheiten" int ARRAY, + "abwesenheiten" int ARRAY, UNIQUE ("personal_nummer", "woche_start") ); DROP TABLE IF EXISTS "abwesenheit"; CREATE TABLE "abwesenheit" ( "counter_id" bigserial PRIMARY KEY, - "card_uid" varchar(255), - "abwesenheit_typ" int2, - "datum_from" timestamptz DEFAULT NOW()::DATE, - "datum_to" timestamptz + "card_uid" varchar(255) NOT NULL, + "abwesenheit_typ" int2 NOT NULL, + "datum_from" timestamptz DEFAULT NOW()::DATE NOT NULL, + "datum_to" timestamptz NOT NULL ); DROP TABLE IF EXISTS "s_abwesenheit_typen"; CREATE TABLE "s_abwesenheit_typen" ( - "abwesenheit_id" int2 PRIMARY KEY, - "abwesenheit_name" varchar(255), - "arbeitszeit_equivalent" float4 + "abwesenheit_id" int2 PRIMARY KEY NOT NULL, + "abwesenheit_name" varchar(255) NOT NULL, + "arbeitszeit_equivalent" float4 NOT NULL +); +COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; -1=Arbeitszeit auffüllen; <=1 - 100 => Arbeitszeit pro Tag prozentual'; + +DROP TABLE IF EXISTS "s_feiertage"; +CREATE TABLE "s_feiertage" ( + "counter_id" serial PRIMARY KEY NOT NULL, + "datum" date NOT NULL, + "name" varchar(100) NOT NULL, + "wiederholen" smallint NOT NULL DEFAULT 0, + "arbeitszeit_equivalent" smallint NOT NULL DEFAULT 100 +); + +CREATE UNIQUE index feiertage_unique_pro_jahr on s_feiertage ( + extract ( year from datum ), + name ); -- Adds crypto extension diff --git a/DB/initdb/02_sample_data.sql b/DB/initdb/02_sample_data.sql index a0ee8f3..ea7fcf7 100755 --- a/DB/initdb/02_sample_data.sql +++ b/DB/initdb/02_sample_data.sql @@ -5,4 +5,4 @@ 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); +INSERT INTO "s_abwesenheit_typen" ("abwesenheit_id", "abwesenheit_name", "arbeitszeit_equivalent") VALUES (1, 'Urlaub', 100), (2, 'Krank', 100), (3, 'Kurzarbeit', -1), (4, 'Urlaub untertags', 50); diff --git a/DB/initdb/03_create_user.sh b/DB/initdb/03_create_user.sh index c850d00..42be086 100755 --- a/DB/initdb/03_create_user.sh +++ b/DB/initdb/03_create_user.sh @@ -5,10 +5,14 @@ echo "Creating PostgreSQL user and setting permissions... $POSTGRES_USER for API psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE USER $POSTGRES_API_USER WITH ENCRYPTED PASSWORD '$POSTGRES_API_PASS'; +EOSQL + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL 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, 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 SELECT, INSERT, UPDATE ON anwesenheit, abwesenheit, user_password, wochen_report, s_feiertage TO $POSTGRES_API_USER; + GRANT DELETE ON abwesenheit TO $POSTGRES_API_USER; + GRANT SELECT ON s_personal_daten, s_abwesenheit_typen, s_anwesenheit_typen, s_feiertage 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 deleted file mode 100644 index 256e380..0000000 --- a/Docker/.env.example +++ /dev/null @@ -1,11 +0,0 @@ -POSTGRES_USER=root -POSTGRES_PASSWORD=very_secure -POSTGRES_API_USER=api_nutzer -POSTGRES_API_PASS=password -POSTGRES_PATH=../DB -POSTGRES_DB=arbeitszeitmessung -EXPOSED_PORT=8000 -TZ=Europe/Berlin -PGTZ=Europe/Berlin -API_TOKEN=dont_access -EMPTY_DAYS=false diff --git a/Docker/docker-compose.dev.yml b/Docker/docker-compose.dev.yml index 2554fbb..b5a413b 100644 --- a/Docker/docker-compose.dev.yml +++ b/Docker/docker-compose.dev.yml @@ -1,12 +1,6 @@ name: arbeitszeitmessung-dev services: db: - image: postgres:16 - restart: unless-stopped - env_file: - - .env - environment: - PGDATA: /var/lib/postgresql/data/pg_data volumes: - ${POSTGRES_PATH}:/var/lib/postgresql/data # - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d @@ -19,21 +13,8 @@ services: ports: - 8001:8080 backend: - image: git.letsstein.de/tom/arbeitszeitmessung - restart: unless-stopped - env_file: - - .env environment: - POSTGRES_HOST: db - POSTGRES_DB: ${POSTGRES_DB} - EXPOSED_PORT: ${EXPOSED_PORT} NO_CORS: true - ports: - - ${EXPOSED_PORT}:8080 - volumes: - - ../logs:/app/Backend/logs - depends_on: - - db swagger: image: swaggerapi/swagger-ui diff --git a/Docker/docker-compose.yml b/Docker/docker-compose.yml index f08a18b..a5fc78d 100644 --- a/Docker/docker-compose.yml +++ b/Docker/docker-compose.yml @@ -6,28 +6,31 @@ services: env_file: - .env environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB} + PGTZ: ${TZ} PGDATA: /var/lib/postgresql/data/pg_data volumes: - ${POSTGRES_PATH}:/var/lib/postgresql/data - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d ports: - - 5432:5432 + - ${POSTGRES_PORT}:5432 backend: - image: git.letsstein.de/tom/arbeitszeitmessung + image: git.letsstein.de/tom/arbeitszeitmessung-webserver env_file: - .env environment: POSTGRES_HOST: db POSTGRES_DB: ${POSTGRES_DB} - EXPOSED_PORT: ${EXPOSED_PORT} ports: - - ${EXPOSED_PORT}:8080 + - ${WEB_PORT}:8080 depends_on: - db + - document-creator volumes: - ../logs:/app/Backend/logs restart: unless-stopped + + document-creator: + image: git.letsstein.de/tom/arbeitszeitmessung-doc-creator + container_name: ${TYPST_CONTAINER} + restart: unless-stopped diff --git a/Docker/env.example b/Docker/env.example new file mode 100644 index 0000000..ab2147f --- /dev/null +++ b/Docker/env.example @@ -0,0 +1,12 @@ +POSTGRES_USER=root # Postgres ADMIN Nutzername +POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort +POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung) +POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung) +POSTGRES_PATH=../DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...) +LOG_PATH=../logs # Pfad für Logdatein +POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name +POSTGRES_PORT=127.0.0.1:5432 # Postgres Port will not be exposed by default. regex:^[0-9]{1,5}$ +TZ=Europe/Berlin # Zeitzone +API_TOKEN=dont_access # API Token für ESP Endpoints +WEB_PORT=8000 # Port from which Arbeitszeitmessung should be accessable regex:^[0-9]{1,5}$ +TYPST_CONTAINER=arbeitszeitmessung-doc-creator # Name of the pdf compiler container diff --git a/DocumentCreator/Dockerfile b/DocumentCreator/Dockerfile new file mode 100644 index 0000000..e132f75 --- /dev/null +++ b/DocumentCreator/Dockerfile @@ -0,0 +1,7 @@ +FROM ghcr.io/typst/typst:0.14.0 + +WORKDIR /app +COPY ./templates /app/templates +COPY ./static /app/static + +ENTRYPOINT ["sh", "-c", "while true; do sleep 3600; done"] diff --git a/DocumentCreator/static/logo.png b/DocumentCreator/static/logo.png new file mode 100644 index 0000000..53694f9 Binary files /dev/null and b/DocumentCreator/static/logo.png differ diff --git a/DocumentCreator/templates/abrechnung.typ b/DocumentCreator/templates/abrechnung.typ new file mode 100644 index 0000000..0735264 --- /dev/null +++ b/DocumentCreator/templates/abrechnung.typ @@ -0,0 +1,97 @@ +#let table-header(..headers) = { + table.header( + ..headers.pos().map(h => strong(h)) + ) +} + + +#let abrechnung(meta, days) = { + set page(paper: "a4", margin: (x:1.5cm, y:2.25cm), + footer:[#grid( + columns: (3fr, .65fr), + align: left + horizon, + inset: .5em, + [#meta.EmployeeName -- #meta.TimeRange], grid.cell(rowspan: 2)[#image("/static/logo.png")], + [Arbeitszeitrechnung maschinell erstellt am #meta.CurrentTimestamp], + ) + ]) + set text(font: "Noto Sans", size:10pt, fill: luma(10%)) + set table( + stroke: 0.5pt + luma(10%), + inset: .5em, + align: center + horizon, + ) + show text: it => { + if it.text == "0min"{ + text(oklch(70.8%, 0, 0deg))[#it] + }else if it.text.starts-with("-"){ + text(red)[#it] + }else{ + it + } + } + + + [= Abrechnung Arbeitszeit -- #meta.EmployeeName] + + [Zeitraum: #meta.TimeRange] + + table( + columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, .875fr, 1.25fr), + fill: (x, y) => + if y == 0 { oklch(87%, 0, 0deg) }, + table-header( + [Datum], [Kommen], [Gehen], [Arbeitsart], [Stunden], [Kurzarbeit], [Pause], [Überstunden] + ), + .. for day in days { + ( + [#day.Date], + if day.DayParts.len() == 0{ + table.cell(colspan: 3)[Keine Buchungen] + }else if day.DayParts.len() == 1 and not day.DayParts.first().IsWorkDay{ + table.cell(colspan: 3)[#day.DayParts.first().WorkType] + } + else { + table.cell(colspan: 3, inset: 0em)[ + #table( + columns: (1fr, 1fr, 1fr), + .. for Zeit in day.DayParts { + ( + if Zeit.IsWorkDay{ + ( + table.cell()[#Zeit.BookingFrom], + table.cell()[#Zeit.BookingTo], + table.cell()[#Zeit.WorkType], + ) + }else{ + (table.cell(colspan: 3)[#Zeit.WorkType],) + } + ) + }, + ) + ] + }, + [#day.Worktime], + [#day.Kurzarbeit], + [#day.Pausetime], + [#day.Overtime], + ) + if day.IsFriday { + ( table.cell(colspan: 8, fill: oklch(87%, 0, 0deg))[Wochenende], ) // note the trailing comma + } + } + ) + + table( + columns: (3fr, 1fr), + align: right, + inset: (x: .25em, y:.75em), + stroke: none, + table.hline(start: 0, end: 2, stroke: stroke(dash:"dashed", thickness:.5pt)), + [Arbeitszeit :], table.cell(align: left)[#meta.WorkTime], + [Kurzarbeit :], table.cell(align: left)[#meta.Kurzarbeit], + [Überstunden :], table.cell(align: left)[#meta.Overtime], + [Überstunden lfd. :],table.cell(align: left)[#meta.OvertimeTotal], + table.hline(start: 0, end: 2), +) +} diff --git a/Makefile b/Makefile index 283ab33..8de2724 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ generateFrontend: backend: generateFrontend login_registry - docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:latest Backend --load #--push + docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:latest Backend --push # docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:${GIT_COMMIT} Backend //--push test: @@ -52,3 +52,11 @@ test: scan: test $(MAKE) -C Backend scan + +Docker/.env: + cp Docker/env.example Docker/.env + echo "Konfigurations Datei erstellt (./Docker/.env), bitte zuerst ausfüllen und danach erneut 'make install' ausführen" + exit 0 + +install: Docker/.env + echo "Install" diff --git a/Readme.md b/Readme.md index 20c4f98..2f4bbdd 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,6 @@ # Arbeitszeitmessung -[![Quality Gate Status](https://sonar.letsstein.de/api/project_badges/measure?project=arbeitszeitmessung&metric=alert_status&token=sqb_f8e5ad702b23aa4631a29b99c2f8030caf7d4e05)](https://sonar.letsstein.de/dashboard?id=arbeitszeitmessung) +[![Quality Gate Status](https://sonar.letsstein.de/api/project_badges/measure?project=arbeitszeitmessung&metric=alert_status&token=sqb_253028eff30aff24f32b437cd6c484c511b5c33f)](https://sonar.letsstein.de/dashboard?id=arbeitszeitmessung) bis jetzt ein einfaches Backend mit PostgreSQL Datenbank und GO Webserver um Arbeitszeitbuchungen per HTTP PUT einzufügen diff --git a/db.sql b/db.sql index cdc1beb..7f2dfff 100644 --- a/db.sql +++ b/db.sql @@ -176,15 +176,15 @@ sample_bookings AS ( (d.work_date + make_time(16, floor(random()*50)::int, 0))::timestamptz AS ts, 1 AS anwesenheit_typ FROM days d -), -ins_anw AS ( - -- insert only bookings up to now (prevents future times on today) - INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id) - SELECT ts, card_uid, check_in_out, geraet_id - FROM sample_bookings - WHERE ts <= NOW() - RETURNING 1 ) + +-- insert only bookings up to now (prevents future times on today) +INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id) +SELECT ts, card_uid, check_in_out, geraet_id +FROM sample_bookings +WHERE ts <= NOW() +RETURNING 1; + -- now insert absences (uses the same days CTE) INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum) SELECT @@ -247,15 +247,13 @@ all_bookings AS ( SELECT * FROM base_bookings UNION ALL SELECT * FROM pause_bookings -), -ins_anw AS ( - INSERT INTO anwesenheit ("timestamp", "card_uid", "check_in_out", "geraet_id", "anwesenheit_typ") - SELECT ts, card_uid, check_in_out, geraet_id, 1 as anwesenheit_typ - FROM all_bookings - WHERE ts <= NOW() - ORDER BY work_date, ts - RETURNING 1 ) +INSERT INTO anwesenheit ("timestamp", "card_uid", "check_in_out", "geraet_id", "anwesenheit_typ") +SELECT ts, card_uid, check_in_out, geraet_id, 1 as anwesenheit_typ +FROM all_bookings +WHERE ts <= NOW() +ORDER BY work_date, ts; + INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum) SELECT d.card_uid, diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..114c7b5 --- /dev/null +++ b/install.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -e + +envFile=Docker/.env +envExample=Docker/env.example + +echo "Checking Docker installation..." +if ! command -v docker >/dev/null 2>&1; then + echo "Docker not found. Install Docker? [y/N]" + read -r install_docker + if [[ "$install_docker" =~ ^[Yy]$ ]]; then + curl -fsSL https://get.docker.com | sh + else + echo "Docker is required. Exiting." + exit 1 + fi +else + echo "Docker is already installed." +fi + +echo "Checking Docker Compose..." +if ! docker compose version >/dev/null 2>&1; then + echo "Docker Compose plugin missing. You may need to update Docker." + exit 1 +fi + +echo "Preparing .env file..." +if [ ! -f $envFile ]; then + if [ -f $envExample ]; then + echo ".env not found. Creating interactively from .env.example." + > $envFile + + while IFS= read -r line; do + + #ignore empty lines and comments + [[ "$line" =~ ^#.*$ || -z "$line" ]] && continue + + + key=$(printf "%s" "$line" | cut -d '=' -f 1) + rest=$(printf "%s" "$line" | cut -d '=' -f 2-) + + # extract inline comment portion + comment=$(printf "%s" "$rest" | sed -n 's/.*# \(.*\)$/\1/p') + raw_val=$(printf "%s" "$rest" | sed 's/ *#.*//') + default_value=$(printf "%s" "$raw_val" | sed 's/"//g') + + regex="" + if [[ "$comment" =~ regex:(.*)$ ]]; then + regex="${BASH_REMATCH[1]}" + fi + + comment=$(printf "%s" "$comment" | sed 's/ regex:.*//') + + while true; do + if [ -z "$comment" ]; then + printf "Value for $key - $comment (default: $default_value" + else + printf "Value for $key (default: $default_value" + fi + if [ -n "$regex" ]; then + printf ", must match: %s" "$regex" + fi + printf "):\n" + + read user_input < /dev/tty + + # empty input -> take default + [ -z "$user_input" ] && user_input="$default_value" + + printf "\e[A$user_input\n" + + # validate + if [ -n "$regex" ]; then + if [[ "$user_input" =~ $regex ]]; then + echo "$key=$user_input" >> $envFile + break + else + printf "Invalid value. Does not match regex: %s\n" "$regex" + continue + fi + else + echo "$key=$user_input" >> $envFile + break + fi + done + + done < $envExample + + echo ".env created." + else + echo "No .env or .env.example found." + echo "Creating an empty .env file for manual editing." + touch $envFile + fi +else + echo "Using existing .env. (found at $envFile)" +fi + +echo "Start containers with docker compose up -d? [y/N]" +read -r start_containers +if [[ "$start_containers" =~ ^[Yy]$ ]]; then + cd Docker + mkdir ../logs + docker compose up -d + echo "Containers started." +else + echo "You can start them manually with: docker compose up -d" +fi diff --git a/migrations/20251013212224_buchungs_array.down.sql b/migrations/20251013212224_buchungs_array.down.sql new file mode 100644 index 0000000..b403a4c --- /dev/null +++ b/migrations/20251013212224_buchungs_array.down.sql @@ -0,0 +1,4 @@ +-- reverse: modify "wochen_report" table +ALTER TABLE "wochen_report" DROP COLUMN "abwesenheiten", DROP COLUMN "anwesenheiten", ALTER COLUMN "arbeitszeit" DROP NOT NULL, ALTER COLUMN "ueberstunden" DROP NOT NULL, ALTER COLUMN "woche_start" DROP NOT NULL; +-- reverse: modify "abwesenheit" table +ALTER TABLE "abwesenheit" ALTER COLUMN "datum_to" DROP NOT NULL; diff --git a/migrations/20251013212224_buchungs_array.up.sql b/migrations/20251013212224_buchungs_array.up.sql new file mode 100644 index 0000000..1baa199 --- /dev/null +++ b/migrations/20251013212224_buchungs_array.up.sql @@ -0,0 +1,4 @@ +-- modify "abwesenheit" table +ALTER TABLE "abwesenheit" ALTER COLUMN "datum_to" SET NOT NULL; +-- modify "wochen_report" table +ALTER TABLE "wochen_report" ALTER COLUMN "woche_start" SET NOT NULL, ALTER COLUMN "ueberstunden" SET NOT NULL, ALTER COLUMN "arbeitszeit" SET NOT NULL, ADD COLUMN "anwesenheiten" integer[] NULL, ADD COLUMN "abwesenheiten" integer[] NULL; diff --git a/migrations/20251217215955_feiertage.down.sql b/migrations/20251217215955_feiertage.down.sql new file mode 100644 index 0000000..c3c5ee8 --- /dev/null +++ b/migrations/20251217215955_feiertage.down.sql @@ -0,0 +1,6 @@ +-- reverse: create index "feiertage_unique_pro_jahr" to table: "s_feiertage" +DROP INDEX "feiertage_unique_pro_jahr"; +-- reverse: create "s_feiertage" table +DROP TABLE "s_feiertage"; +-- reverse: set comment to column: "arbeitszeit_equivalent" on table: "s_abwesenheit_typen" +COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; 1=Arbeitszeit auffüllen; 2=Arbeitszeit austauschen'; diff --git a/migrations/20251217215955_feiertage.up.sql b/migrations/20251217215955_feiertage.up.sql new file mode 100644 index 0000000..69072a0 --- /dev/null +++ b/migrations/20251217215955_feiertage.up.sql @@ -0,0 +1,13 @@ +-- set comment to column: "arbeitszeit_equivalent" on table: "s_abwesenheit_typen" +COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; -1=Arbeitszeit auffüllen; <=1 - 100 => Arbeitszeit pro Tag prozentual'; +-- create "s_feiertage" table +CREATE TABLE "s_feiertage" ( + "counter_id" serial NOT NULL, + "datum" date NOT NULL, + "name" character varying(100) NOT NULL, + "wiederholen" smallint NOT NULL DEFAULT 0, + "arbeitszeit_equivalent" smallint NOT NULL DEFAULT 100, + PRIMARY KEY ("counter_id") +); +-- create index "feiertage_unique_pro_jahr" to table: "s_feiertage" +CREATE UNIQUE INDEX "feiertage_unique_pro_jahr" ON "s_feiertage" ((EXTRACT(year FROM datum)), "name"); diff --git a/migrations/atlas.sum b/migrations/atlas.sum index 4950d34..6030b9b 100644 --- a/migrations/atlas.sum +++ b/migrations/atlas.sum @@ -1,15 +1,10 @@ -h1:3AxgD8mnu/F+JGtJu9FZvA9Ro0UUtGPgyjskKtfTYUQ= -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= -20250903233030_non_null_contraints.down.sql h1:42TZzPsji2Ze50k6sLwgIuNo4Trk3m3ni/aIfQJ97dE= -20250903233030_non_null_contraints.up.sql h1:k6zR5YNSAP4fo5QEc58KZ0LxvEz1nl0X/AAcZ+TG3I4= -20250904114004_intervals.down.sql h1:SquJAPinzFIRN6fJjLLIRsz59Tyr4RwGiGuOFI/N1SQ= -20250904114004_intervals.up.sql h1:AFqncTGOiEZVBbhWFqN2zlQ7DyhybB5wJr6a36Atk1E= -20250916093608_kurzarbeit.down.sql h1:ljM1a1pQCxOQiXRaXU04GC4V9yy2y20x5eUNQ/zyx+o= -20250916093608_kurzarbeit.up.sql h1:pTiw0VfGaf26mhJg4wf98Fqwn1kShJ+PiN2PiM4q1kk= +h1:1lrLZOm9nGe6v1/TrR1Ij8LBRDCY2igXwwUB+XqEIrc= +20250901201159_initial.up.sql h1:Mb1RlVdFvcxqU9HrSK6oNeURqFa3O4KzB3rDa+6+3gc= +20250901201250_control_tables.up.sql h1:a5LATgR/CRiC4GsqxkJ94TyJOxeTcW74eCnodIy+c1E= +20250901201710_triggers_extension.up.sql h1:z9b6Hk9btE2Ns4mU7B16HjvYBP6EEwHAXVlvPpkn978= +20250903221313_overtime.up.sql h1:t/B435ShW5ZEnzC81jRABWVZ5gNm7tPZPnOO6/ZY6ow= +20250903233030_non_null_contraints.up.sql h1:YKeYgazfh+jPyh7hFT/pV+By8eHnk1taXnlgSLyXSA0= +20250904114004_intervals.up.sql h1:gDdN8cJ4xH1vQhAbbhqD5lwdyEO1N9EIqEYkmWGiWIU= +20250916093608_kurzarbeit.up.sql h1:yDAAMLyUXz6b7+MI6XK/HZMPzutKoT2NNNOCjFaqSts= +20251013212224_buchungs_array.up.sql h1:mbhvnwMUkEFFQQ41NC47auqxbtvNkztziWvpLDFm6tA= +20251217215955_feiertage.up.sql h1:PipbYvfL8YtsidgbJ3oEHYrmiNzffQ7veyaGAxINltE= diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18cb121..a07ce83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,42 @@ importers: tailwindcss: specifier: ^4.1.12 version: 4.1.12 + devDependencies: + '@iconify-json/material-symbols-light': + specifier: ^1.2.33 + version: 1.2.50 + '@iconify/tailwind4': + specifier: ^1.0.6 + version: 1.2.0(tailwindcss@4.1.12) + prettier: + specifier: ^3.6.2 + version: 3.7.4 packages: + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@cyberalien/svg-utils@1.0.11': + resolution: {integrity: sha512-qEE9mnyI+avfGT3emKuRs3ucYkITeaV0Xi7VlYN41f+uGnZBecQP3jwz/AF437H9J4Q7qPClHKm4NiTYpNE6hA==} + + '@iconify-json/material-symbols-light@1.2.50': + resolution: {integrity: sha512-Ehvmar2TPoYxmKgB5szeIMlmvA/mIc7gzUoQ5/AWFG+N6d4T53uCHwxnXFf1nXPWlpf0+cv26AXMJC6W5mkdrQ==} + + '@iconify/tailwind4@1.2.0': + resolution: {integrity: sha512-+t7XqfojOB0zzZdd8gV7IQZGq1AaIHTlsxMVzagxYR0hAlJCLUD63o3iSlNKRMH3ZR7gZ8y5c9dJ7J431avRbA==} + peerDependencies: + tailwindcss: '>= 4.0.0' + + '@iconify/tools@5.0.1': + resolution: {integrity: sha512-/znhBN9WIpJd9UtKhyEDfRKwNo8rrOy8dShF8bwSZ1i27ukTSHjeS6bmVK4tTYBYriwFhBf70JT6g8GIRwFvbw==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -208,6 +241,14 @@ packages: resolution: {integrity: sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==} engines: {node: '>= 10'} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -216,6 +257,32 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -225,10 +292,30 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -319,6 +406,12 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -336,6 +429,13 @@ packages: engines: {node: '>=10'} hasBin: true + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + modern-tar@0.7.3: + resolution: {integrity: sha512-4W79zekKGyYU4JXVmB78DOscMFaJth2gGhgfTl2alWE4rNe3nf4N2pqenQ0rEtIewrnD79M687Ouba3YGTLOvg==} + engines: {node: '>=18.0.0'} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -343,6 +443,15 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -350,10 +459,26 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + + sax@1.4.3: + resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + svgo@4.0.0: + resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==} + engines: {node: '>=16'} + hasBin: true + tailwindcss@4.1.12: resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==} @@ -365,16 +490,61 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} snapshots: + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + + '@cyberalien/svg-utils@1.0.11': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/material-symbols-light@1.2.50': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/tailwind4@1.2.0(tailwindcss@4.1.12)': + dependencies: + '@iconify/tools': 5.0.1 + '@iconify/types': 2.0.0 + '@iconify/utils': 3.1.0 + tailwindcss: 4.1.12 + + '@iconify/tools@5.0.1': + dependencies: + '@cyberalien/svg-utils': 1.0.11 + '@iconify/types': 2.0.0 + '@iconify/utils': 3.1.0 + fflate: 0.8.2 + modern-tar: 0.7.3 + pathe: 2.0.3 + svgo: 4.0.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.0 + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -532,21 +702,75 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.12 '@tailwindcss/oxide-win32-x64-msvc': 4.1.12 + acorn@8.15.0: {} + + boolbase@1.0.0: {} + braces@3.0.3: dependencies: fill-range: 7.1.1 chownr@3.0.0: {} + commander@11.1.0: {} + + confbox@0.1.8: {} + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + detect-libc@1.0.3: {} detect-libc@2.0.4: {} + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 tapable: 2.2.2 + entities@4.5.0: {} + + fflate@0.8.2: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -612,6 +836,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mdn-data@2.0.28: {} + + mdn-data@2.12.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -625,16 +853,53 @@ snapshots: mkdirp@3.0.1: {} + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + modern-tar@0.7.3: {} + mri@1.2.0: {} node-addon-api@7.1.1: {} + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + package-manager-detector@1.6.0: {} + + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + prettier@3.7.4: {} + + sax@1.4.3: {} + source-map-js@1.2.1: {} + svgo@4.0.0: + dependencies: + commander: 11.1.0 + css-select: 5.2.2 + css-tree: 3.1.0 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.4.3 + tailwindcss@4.1.12: {} tapable@2.2.2: {} @@ -648,8 +913,12 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + tinyexec@1.0.2: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + ufo@1.6.1: {} + yallist@5.0.0: {}