From 5001f24d9b62aae86b752e35a7a1d5e0f5ecb8cb Mon Sep 17 00:00:00 2001 From: tom Date: Mon, 13 Oct 2025 22:33:48 +0200 Subject: [PATCH 01/35] implemented log levels and structured log with slog --- Backend/endpoints/pdf.go | 2 +- Backend/main.go | 24 ++++++++++++---------- Backend/templates/headerComponent_templ.go | 2 +- Backend/templates/pages_templ.go | 2 +- Backend/templates/pdf_templ.go | 2 +- Backend/templates/teamComponents_templ.go | 2 +- Backend/templates/timeComponents_templ.go | 2 +- Backend/templates/timePage_templ.go | 2 +- Makefile | 2 +- 9 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Backend/endpoints/pdf.go b/Backend/endpoints/pdf.go index 87beb06..f3ebc12 100644 --- a/Backend/endpoints/pdf.go +++ b/Backend/endpoints/pdf.go @@ -11,7 +11,7 @@ import ( func PDFHandler(w http.ResponseWriter, r *http.Request) { helper.RequiresLogin(Session, w, r) - startDate, err := parseTimestamp(r, "start", time.Now().Format("2006-01-02")) + startDate, err := parseTimestamp(r, "start_date", 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) diff --git a/Backend/main.go b/Backend/main.go index 35395e2..15d4a96 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")) @@ -58,14 +59,15 @@ func main() { 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/templates/headerComponent_templ.go b/Backend/templates/headerComponent_templ.go index aeef6ec..8e59160 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.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/pages_templ.go b/Backend/templates/pages_templ.go index 491784c..ad17982 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.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/pdf_templ.go b/Backend/templates/pdf_templ.go index 3c73ded..321301b 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.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index ec539f2..c1d475e 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.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/timeComponents_templ.go b/Backend/templates/timeComponents_templ.go index 835b46a..c7ad17e 100644 --- a/Backend/templates/timeComponents_templ.go +++ b/Backend/templates/timeComponents_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.924 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/timePage_templ.go b/Backend/templates/timePage_templ.go index d668290..dd29246 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.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Makefile b/Makefile index 283ab33..58d90fb 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: -- 2.49.1 From 0d7696cbc6b9e3eef8f075f47422de90e68be8fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Tue, 14 Oct 2025 01:05:02 +0200 Subject: [PATCH 02/35] adding more logging + working on displaying if a workday was submitted --- Backend/models/booking.go | 22 +++++ Backend/models/workDay.go | 2 + Backend/models/workWeek.go | 91 ++++++++++++++----- Backend/templates/headerComponent_templ.go | 2 +- Backend/templates/pages_templ.go | 2 +- Backend/templates/pdf_templ.go | 2 +- Backend/templates/teamComponents.templ | 2 +- Backend/templates/teamComponents_templ.go | 4 +- Backend/templates/timeComponents.templ | 3 + Backend/templates/timeComponents_templ.go | 16 +++- Backend/templates/timePage_templ.go | 2 +- DB/initdb/01_schema.sql | 28 +++--- .../20251013212224_buchungs_array.down.sql | 4 + .../20251013212224_buchungs_array.up.sql | 4 + migrations/atlas.sum | 24 ++--- 15 files changed, 146 insertions(+), 62 deletions(-) create mode 100644 migrations/20251013212224_buchungs_array.down.sql create mode 100644 migrations/20251013212224_buchungs_array.up.sql 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/workDay.go b/Backend/models/workDay.go index 9780ce7..5ae30a0 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "log/slog" "sort" "time" ) @@ -110,6 +111,7 @@ func (d *WorkDay) TimeWorkReal(u User) time.Duration { if helper.IsSameDate(d.Date(), time.Now()) && len(d.Bookings)%2 == 1 { d.realWorkTime += time.Since(lastBooking.Timestamp.Local()) } + slog.Debug("Calculated RealWorkTime for user", "user", u, slog.String("worktime", d.realWorkTime.String())) return d.realWorkTime } diff --git a/Backend/models/workWeek.go b/Backend/models/workWeek.go index 634d6fb..beaf504 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,17 @@ 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("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) + work, _ := day.TimePauseReal(w.User) + w.Worktime += work + w.WorkTimeVirtual += day.TimeWorkVirtual(w.User) } - log.Println("Calculated new worktime", w.Worktime) - w.Overtime = w.Worktime - w.User.ArbeitszeitProWoche() + slog.Debug("Got worktime for user", "user", w.User, "worktime", w.Worktime.String(), "virtualWorkTime", w.WorkTimeVirtual.String()) + + w.Overtime = w.WorkTimeVirtual - w.User.ArbeitszeitProWoche() w.Worktime = w.Worktime.Round(time.Minute) w.Overtime = w.Overtime.Round(time.Minute) @@ -65,8 +68,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 +150,70 @@ func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek { var ErrRunningWeek = errors.New("Week is in running week") +func (w *WorkWeek) GetBookingIds() (anwesenheitsIds, abwesenheitsIds []int64, err error) { + qStr, err := DB.Prepare(` + SELECT + (SELECT array_agg(counter_id ORDER BY counter_id) + FROM anwesenheit + WHERE card_uid = $1 + AND timestamp::DATE >= $2 + AND timestamp::DATE < $3) AS anwesenheit, + + (SELECT array_agg(counter_id ORDER BY counter_id) + FROM abwesenheit + WHERE card_uid = $1 + AND datum_from < $3 + AND datum_to >= $2) AS abwesenheit; + `) + if err != nil { + return nil, nil, err + } + defer qStr.Close() + + slog.Debug("Inserting parameters into qStr:", "user card_uid", w.User.CardUID, "week_start", w.WeekStart, "week_end", w.WeekStart.AddDate(0, 0, 5)) + + err = qStr.QueryRow(w.User.CardUID, w.WeekStart, w.WeekStart.AddDate(0, 0, 5)).Scan(pq.Array(&anwesenheitsIds), pq.Array(&abwesenheitsIds)) + if err != nil { + return anwesenheitsIds, abwesenheitsIds, err + } + return anwesenheitsIds, abwesenheitsIds, nil +} + // creates a new entry in the woche_report table with the given workweek func (w *WorkWeek) SendWeek() error { var qStr *sql.Stmt var err error + slog.Info("Sending workWeek to team head", "week", w.WeekStart.String()) + + anwBookings, awBookings, err := w.GetBookingIds() + if err != nil { + slog.Warn("Error querying bookings from work week", slog.Any("error", err)) + return err + } + + slog.Debug("Recieved Booking Ids", "anwesenheiten", anwBookings) + if time.Since(w.WeekStart) < 5*24*time.Hour { - log.Println("Cannot send week, because it's the running week!") + slog.Warn("Cannot send week, because it's the running week!") return ErrRunningWeek } if w.CheckStatus() != WeekStatusNone { - qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE, arbeitszeit = make_interval(secs => $3::numeric / 1000000000), ueberstunden = make_interval(secs => $4::numeric / 1000000000) WHERE personal_nummer = $1 AND woche_start = $2;`) + qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE, arbeitszeit = make_interval(secs => $3::numeric / 1000000000), ueberstunden = make_interval(secs => $4::numeric / 1000000000), anwesenheiten=$5, abwesenheiten=$6 WHERE personal_nummer = $1 AND woche_start = $2;`) if err != nil { - log.Println("Error preparing SQL statement", err) + slog.Warn("Error preparing SQL statement", "error", err) return err } } else { - qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden) VALUES ($1, $2, make_interval(secs => $3::numeric / 1000000000), make_interval(secs => $4::numeric / 1000000000));`) + qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden, anwesenheiten, abwesenheiten) VALUES ($1, $2, make_interval(secs => $3::numeric / 1000000000), make_interval(secs => $4::numeric / 1000000000), $5, $6);`) if err != nil { - log.Println("Error preparing SQL statement", err) + slog.Warn("Error preparing SQL statement", "error", err) return err } } - _, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, int64(w.Worktime), int64(w.Overtime)) + + _, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, int64(w.Worktime), int64(w.Overtime), pq.Array(anwBookings), pq.Array(awBookings)) if err != nil { log.Println("Error executing query!", err) return err diff --git a/Backend/templates/headerComponent_templ.go b/Backend/templates/headerComponent_templ.go index 8e59160..aeef6ec 100644 --- a/Backend/templates/headerComponent_templ.go +++ b/Backend/templates/headerComponent_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/pages_templ.go b/Backend/templates/pages_templ.go index ad17982..491784c 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.943 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/pdf_templ.go b/Backend/templates/pdf_templ.go index 321301b..3c73ded 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.943 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/teamComponents.templ b/Backend/templates/teamComponents.templ index e5738b9..d1dd820 100644 --- a/Backend/templates/teamComponents.templ +++ b/Backend/templates/teamComponents.templ @@ -81,7 +81,7 @@ templ weekDayComponent(user models.User, day models.WorkDay) { templ workWeekComponent(week models.WorkWeek, onlyAccept bool) { {{ year, kw := week.WeekStart.ISOWeek() - progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100 + progress := (float32(week.WorkTimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100 }}
diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index c1d475e..1c0e46b 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.943 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -346,7 +346,7 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { ctx = templ.ClearChildren(ctx) year, kw := week.WeekStart.ISOWeek() - progress := (float32(week.Worktime.Hours()) / week.User.ArbeitszeitPerWoche) * 100 + progress := (float32(week.WorkTimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100 templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err diff --git a/Backend/templates/timeComponents.templ b/Backend/templates/timeComponents.templ index 799ebda..9730101 100644 --- a/Backend/templates/timeComponents.templ +++ b/Backend/templates/timeComponents.templ @@ -120,6 +120,9 @@ templ bookingComponent(booking models.Booking) { { booking.GetBookingType() }

+ if booking.IsSubmittedAndChecked() { +

submitted

+ }
} diff --git a/Backend/templates/timeComponents_templ.go b/Backend/templates/timeComponents_templ.go index c7ad17e..f024321 100644 --- a/Backend/templates/timeComponents_templ.go +++ b/Backend/templates/timeComponents_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.943 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -568,7 +568,17 @@ func bookingComponent(booking models.Booking) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

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

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

submitted

") + 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 } @@ -597,7 +607,7 @@ func LegendComponent() templ.Component { templ_7745c5c3_Var31 = 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, 42, "
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.go b/Backend/templates/timePage_templ.go index dd29246..d668290 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.943 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/DB/initdb/01_schema.sql b/DB/initdb/01_schema.sql index e3e795f..21e48ea 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,30 +78,34 @@ EXECUTE FUNCTION update_zuletzt_geandert(); DROP TABLE IF EXISTS "wochen_report"; CREATE TABLE "wochen_report" ( "id" serial PRIMARY KEY, - "personal_nummer" int4, - "woche_start" date, + "personal_nummer" int4 NOT NULL, + "woche_start" date NOT NULL, "bestaetigt" bool DEFAULT FALSE, - "arbeitszeit" interval, - "ueberstunden" interval, + "arbeitszeit" interval NOT NULL, + "ueberstunden" interval NOT NULL, + "anwesenheiten" int ARRAY, + "abwesenheiten" int ARRAY, UNIQUE ("personal_nummer", "woche_start") ); DROP TABLE IF EXISTS "abwesenheit"; CREATE TABLE "abwesenheit" ( "counter_id" bigserial PRIMARY KEY, - "card_uid" varchar(255), - "abwesenheit_typ" int2, - "datum_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; 2=Arbeitszeit austauschen'; + -- Adds crypto extension CREATE EXTENSION IF NOT EXISTS pgcrypto; 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/atlas.sum b/migrations/atlas.sum index 4950d34..114628d 100644 --- a/migrations/atlas.sum +++ b/migrations/atlas.sum @@ -1,15 +1,9 @@ -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:gE7ikkZS7bQbedAjVspQKftSo5ODij2eiQGWbfQEYmI= +20250901201159_initial.up.sql h1:Mb1RlVdFvcxqU9HrSK6oNeURqFa3O4KzB3rDa+6+3gc= +20250901201250_control_tables.up.sql h1:a5LATgR/CRiC4GsqxkJ94TyJOxeTcW74eCnodIy+c1E= +20250901201710_triggers_extension.up.sql h1:z9b6Hk9btE2Ns4mU7B16HjvYBP6EEwHAXVlvPpkn978= +20250903221313_overtime.up.sql h1:t/B435ShW5ZEnzC81jRABWVZ5gNm7tPZPnOO6/ZY6ow= +20250903233030_non_null_contraints.up.sql h1: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= -- 2.49.1 From 7eda8eb538911410e41d2d1621da32c3629e924b Mon Sep 17 00:00:00 2001 From: tom Date: Thu, 23 Oct 2025 16:17:49 +0200 Subject: [PATCH 03/35] reworked pdf exporter to use typst --- Backend/endpoints/pdf.go | 53 +++++++++++++++++++++ Backend/go.mod | 3 ++ Backend/go.sum | 8 ++++ Backend/models/workDay.go | 14 +++++- Backend/templates/pdf.templ | 7 ++- Backend/templates/pdf_templ.go | 84 ++++++++++++++++++++++----------- WIR-typst/main.typ | 63 +++++++++++++++++++++++++ WIR-typst/template.pdf | Bin 0 -> 2193 bytes WIR-typst/template.typ | 58 +++++++++++++++++++++++ WIR-typst/test.typ | 36 ++++++++++++++ 10 files changed, 296 insertions(+), 30 deletions(-) create mode 100644 WIR-typst/main.typ create mode 100644 WIR-typst/template.pdf create mode 100644 WIR-typst/template.typ create mode 100644 WIR-typst/test.typ diff --git a/Backend/endpoints/pdf.go b/Backend/endpoints/pdf.go index f3ebc12..4bb8d53 100644 --- a/Backend/endpoints/pdf.go +++ b/Backend/endpoints/pdf.go @@ -9,6 +9,59 @@ import ( "time" ) +type typstMetadata struct { + ISOWeek string `json:"iso-week"` + EmployeeName string `json:"employee-name"` + WorkTime string `json:"worktime"` + Overtime string `json:"overtime"` + OvertimeTotal string `json:"overtime-total"` +} + +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"` +} + +func ConvertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) { + var typstDays []typstDay + for _, day := range days { + var typstDay typstDay + var typstDayParts []typstDayPart + work, pause, overtime := day.GetAllWorkTimesVirtual(u) + typstDay.Date = day.Date().Format("01.02.2006") + typstDay.Worktime = helper.FormatDuration(work) + typstDay.Pausetime = helper.FormatDuration(pause) + typstDay.Overtime = helper.FormatDuration(overtime) + if day.IsWorkDay() { + workDay, _ := day.(*models.WorkDay) + for i := 0; i < len(workDay.Bookings); i += 2 { + var typstDayPart typstDayPart + typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04") + typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04") + typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name + typstDayPart.IsWorkDay = true + typstDayParts = append(typstDayParts, typstDayPart) + } + } else { + absentDay, _ := day.(*models.Absence) + typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name}) + } + typstDay.DayParts = typstDayParts + typstDays = append(typstDays, typstDay) + } + return typstDays, nil +} + func PDFHandler(w http.ResponseWriter, r *http.Request) { helper.RequiresLogin(Session, w, r) startDate, err := parseTimestamp(r, "start_date", time.Now().Format("2006-01-02")) diff --git a/Backend/go.mod b/Backend/go.mod index b993a01..030b122 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -14,8 +14,11 @@ require ( ) require ( + github.com/Dadido3/go-typst v0.3.0 // indirect 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/text v0.23.0 // indirect ) diff --git a/Backend/go.sum b/Backend/go.sum index 6c7522d..a4fe99c 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -1,5 +1,7 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Dadido3/go-typst v0.3.0 h1:Itix2FtQgBiOuHUNqgGUAK11Oo2WMlZGGGpCiQNK1IA= +github.com/Dadido3/go-typst v0.3.0/go.mod h1:QYis9sT70u65kn1SkFfyPRmHsPxgoxWbAixwfPReOZA= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY= @@ -54,6 +56,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/smasher164/xid v0.1.2 h1:erplXSdBRIIw+MrwjJ/m8sLN2XY16UGzpTA0E2Ru6HA= +github.com/smasher164/xid v0.1.2/go.mod h1:tgivm8CQl19fH1c5y+8F4mA+qY6n2i6qDRBlY/6nm+I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= @@ -70,5 +74,9 @@ 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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index 5ae30a0..0fc67aa 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -83,11 +83,23 @@ func (d *WorkDay) Date() time.Time { return d.Day } +func (d *WorkDay) GenerateKurzArbeitBookings(u User) (time.Time, time.Time) { + var timeFrom, timeTo time.Time + if d.workTime >= u.ArbeitszeitProTag() { + return timeFrom, timeTo + } + + timeFrom = d.Bookings[len(d.Bookings)-1].Timestamp.Add(time.Minute) + timeTo = timeFrom.Add(u.ArbeitszeitProTag() - d.workTime) + slog.Debug("Added duration as Kurzarbeit", "date", d.Date().String(), "duration", timeTo.Sub(timeFrom).String()) + + return timeFrom, timeTo +} + func (d *WorkDay) TimeWorkVirtual(u User) time.Duration { if d.IsKurzArbeit() { return u.ArbeitszeitProTag() } - return d.workTime } diff --git a/Backend/templates/pdf.templ b/Backend/templates/pdf.templ index 204e9dd..e740c5e 100644 --- a/Backend/templates/pdf.templ +++ b/Backend/templates/pdf.templ @@ -45,7 +45,12 @@ templ PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays

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

} if workDay.IsKurzArbeit() { -

Kurzarbeit

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

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

+

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

+

Kurzarbeit

} } else { {{ diff --git a/Backend/templates/pdf_templ.go b/Backend/templates/pdf_templ.go index 3c73ded..a7ee638 100644 --- a/Backend/templates/pdf_templ.go +++ b/Backend/templates/pdf_templ.go @@ -252,7 +252,35 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays return templ_7745c5c3_Err } if workDay.IsKurzArbeit() { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

Kurzarbeit

") + + timeFrom, timeTo := workDay.GenerateKurzArbeitBookings(e) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(timeFrom.Format("15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 51, Col: 36} + } + _, 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, 20, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(timeTo.Format("15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 52, Col: 34} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

Kurzarbeit

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -260,25 +288,25 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays } else { absentDay, _ := day.(*models.Absence) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

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

") 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) + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 59, Col: 62} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, 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, 21, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -287,7 +315,7 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -295,7 +323,7 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -303,18 +331,18 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ") 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

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

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, 29, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -338,9 +366,9 @@ 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_Var20 := templ.GetChildren(ctx) + if templ_7745c5c3_Var20 == nil { + templ_7745c5c3_Var20 = templ.NopComponent } ctx = templ.ClearChildren(ctx) @@ -348,38 +376,38 @@ func ColorDuration(d time.Duration, classes string) templ.Component { 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_Var21 = []any{color + " " + classes} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var21...) 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, 31, "\">") 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_Var23 string + templ_7745c5c3_Var23, 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: 81, 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_Var23)) 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, 32, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/WIR-typst/main.typ b/WIR-typst/main.typ new file mode 100644 index 0000000..2f9effa --- /dev/null +++ b/WIR-typst/main.typ @@ -0,0 +1,63 @@ +#set page("a4") +#set text(font: "Lato") + += Stunden + +== Kim Mustermensch + +Zeitraum: 01.10.2025 - 31.10.2025 + +Arbeitszeit: 136h 19min + +Überstunden: -39h 41min + + + + + +// #show table.cell: it => { +// if it.y == 0 { +// set text(white) +// strong(it) +// } else if it.body == [] { +// // Replace empty cells with 'N/A' +// pad(..it.inset)[0min] + +// } else { +// it +// } +// } + +#let subgrid(body) = { + table.cell(colspan: 3, inset: 0em)[ + #table( + columns: (1fr, 1fr, 1fr), + gutter: 0em, + stroke: black, + [..#body] + ) + ] +} + + + "01.09.2025", + "08:07", + "16:28", + "Büro", + "7h 51min", + "30min", + "-9min", + "02.09.2025", + // return work, pause, overtime +table.cell(colspan: 3, inset: 0em)[#table( + columns: (1fr, 1fr, 1fr), + gutter: 0em, + stroke: black, + [08:12], [16:24], [Büro], + [16:30], [17:24], [Homeoffice] + )], + "6h", + "0min", + "-1h 15min" + +) \ No newline at end of file diff --git a/WIR-typst/template.pdf b/WIR-typst/template.pdf new file mode 100644 index 0000000000000000000000000000000000000000..db53c7ae6ae2d912d5f666f3260dc937145546e3 GIT binary patch literal 2193 zcma)8&2Hm15Z*&iTJ#+V6#)_ywnWKtY(dsyW39VJ<6Xlxwu`1Y&=Rf4kwk%_VI`+N zLLXo+z4aY>=nEC-D|ATz$xgDZE|xxiGn{Y8neo{;7&LJ6Qak$<9!Z%hw;1 z2qKi}nH+`b?Coo@x1R$qp95tM4%>hJIA79GAn8=4cuWK+&5?AdfOnPnVJbj2lI|4L zXDQ>;f`k-xcGv*Yjyw-SszoJNH+$8gfdT8+GAb!iJ6%McnsvU^dn~fdkNek4Fo45)8Ix@YU1rgeY9YSg+U3wcTVq zHgLDwHB8H}tOhU|kc%EHf%n0Mk5Tjz(YSX4w&aHjcl}7e)~uEQ(q-jzWP2 z2*m~_WIik0{^okk7)$;74fBlCRY@O>+<=8EseKsuv*9?JT&|Z(YZ&&UFka3HGvD@l z%J!JLH6G@rAbCjthjESSMWMT3JR`yY-wyt1@ strong(text(fill: white, h))) + ) +} + +#set table( + stroke: black, + inset: .5em, + align: center, +) + +#let abrechnung(meta, days) = { + set page(paper: "a4") + + [= Abrechnung Arbeitszeit -- #meta.employee-name] + + [Zeitraum: #meta.Zeitraum + + Arbeitszeit: #user.Arbeitszeit + + Überstunden: #user.Überstunden +] + + table( + columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1.25fr), + fill: (x, y) => + if y == 0 { gray }, + align: center, + table-header( + [Datum], [Kommen], [Gehen], [Arbeitsart], [Stunden], [Pause], [Überstunden] + ), + .. for day in days { + ( + [#day.Date], + table.cell(colspan: 3, inset: 0em)[ + #table( + columns: (1fr, 1fr, 1fr), + gutter: 0em, + stroke: black, + .. for Zeit in day.Zeiten { + ( + [#Zeit.Kommen], + [#Zeit.Gehen], + [#Zeit.Art], + ) + }, + ) + ], + [#day.Arbeitszeit], + [#day.Pause], + [#day.Überstunden], + + ) + + } + ) +} diff --git a/WIR-typst/test.typ b/WIR-typst/test.typ new file mode 100644 index 0000000..555ce61 --- /dev/null +++ b/WIR-typst/test.typ @@ -0,0 +1,36 @@ +#let user = ( + Name: "Mustermensch", + Vorname: "Kim", + Arbeitszeit: "139h 12min", + Überstunden: "-14h 12min" +) + +#let meta = ( + Zeitraum: "01.09.2025 - 30.09.2025", + KW: "26" +) + +#let days = ( + ( + Date: "01.09.2025", + Zeiten: ( + (Kommen: "07:17", Gehen: "14:13", Art: "Büro"), + (Kommen: "14:24", Gehen: "16:13", Art: "Homeoffice") + ), + Arbeitszeit: "7h 32min", + Pause: "34min", + Überstunden: "12min" + ),( + Date: "02.09.2025", + Zeiten: ( + (Kommen: "07:23", Gehen: "14:21", Art: "Büro"), + (Kommen: "14:38", Gehen: "17:13", Art: "Homeoffice") + ), + Arbeitszeit: "6h 22min", + Pause: "45min", + Überstunden: "-23min" + ) +) + +#import "template.typ": abrechnung +#show: doc => abrechnung(meta, user, days) \ No newline at end of file -- 2.49.1 From b6644f358441fe5c38b5b9a0d9065303de62a97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 24 Oct 2025 00:20:51 +0200 Subject: [PATCH 04/35] added pdf generation with typst, working on pdf input form --- Backend/endpoints/pdf.go | 147 ++++++++--- Backend/main.go | 3 +- Backend/static/css/styles.css | 262 +++++++++++++++++++- Backend/static/logo.png | Bin 0 -> 17585 bytes Backend/template.typ | 92 +++++++ Backend/templates/pdf.templ | 40 +++ Backend/templates/pdf_templ.go | 440 +++++++++++++++++++++------------ WIR-typst/template.typ | 58 ----- WIR-typst/test.typ | 36 --- 9 files changed, 790 insertions(+), 288 deletions(-) create mode 100644 Backend/static/logo.png create mode 100644 Backend/template.typ delete mode 100644 WIR-typst/template.typ delete mode 100644 WIR-typst/test.typ diff --git a/Backend/endpoints/pdf.go b/Backend/endpoints/pdf.go index 4bb8d53..eb36e9f 100644 --- a/Backend/endpoints/pdf.go +++ b/Backend/endpoints/pdf.go @@ -4,17 +4,23 @@ import ( "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "arbeitszeitmessung/templates" + "bytes" + "fmt" "log" + "log/slog" "net/http" "time" + + "github.com/Dadido3/go-typst" ) type typstMetadata struct { - ISOWeek string `json:"iso-week"` - EmployeeName string `json:"employee-name"` - WorkTime string `json:"worktime"` - Overtime string `json:"overtime"` - OvertimeTotal string `json:"overtime-total"` + TimeRange string `json:"time-range"` + EmployeeName string `json:"employee-name"` + WorkTime string `json:"worktime"` + Overtime string `json:"overtime"` + OvertimeTotal string `json:"overtime-total"` + CurrentTimestamp string `json:"current-timestamp"` } type typstDayPart struct { @@ -30,38 +36,100 @@ type typstDay struct { Worktime string `json:"worktime"` Pausetime string `json:"pausetime"` Overtime string `json:"overtime"` + IsFriday bool `json:"is-weekend"` } -func ConvertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) { +func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) { var typstDays []typstDay for _, day := range days { - var typstDay typstDay - var typstDayParts []typstDayPart + var thisTypstDay typstDay work, pause, overtime := day.GetAllWorkTimesVirtual(u) - typstDay.Date = day.Date().Format("01.02.2006") - typstDay.Worktime = helper.FormatDuration(work) - typstDay.Pausetime = helper.FormatDuration(pause) - typstDay.Overtime = helper.FormatDuration(overtime) - if day.IsWorkDay() { - workDay, _ := day.(*models.WorkDay) - for i := 0; i < len(workDay.Bookings); i += 2 { - var typstDayPart typstDayPart - typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04") - typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04") - typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name - typstDayPart.IsWorkDay = true - typstDayParts = append(typstDayParts, typstDayPart) - } - } else { - absentDay, _ := day.(*models.Absence) - typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name}) - } - typstDay.DayParts = typstDayParts - typstDays = append(typstDays, typstDay) + thisTypstDay.Date = day.Date().Format("02.01.2006") + thisTypstDay.Worktime = helper.FormatDurationFill(work, true) + thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) + thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true) + thisTypstDay.IsFriday = day.Date().Weekday() == time.Friday + + thisTypstDay.DayParts = convertDayToTypstDayParts(day, u) + typstDays = append(typstDays, thisTypstDay) } return typstDays, nil } +func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDayPart { + var typstDayParts []typstDayPart + if day.IsWorkDay() { + workDay, _ := day.(*models.WorkDay) + for i := 0; i < len(workDay.Bookings); i += 2 { + var typstDayPart typstDayPart + typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04") + typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04") + typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name + typstDayPart.IsWorkDay = true + typstDayParts = append(typstDayParts, typstDayPart) + } + if day.IsKurzArbeit() { + tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user) + typstDayParts = append(typstDayParts, typstDayPart{ + BookingFrom: tsFrom.Format("15:04"), + BookingTo: tsTo.Format("15:04"), + WorkType: "Kurzarbeit", + IsWorkDay: true, + }) + } + } else { + absentDay, _ := day.(*models.Absence) + typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name}) + } + return typstDayParts +} + +func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) { + var markup bytes.Buffer + var output bytes.Buffer + + if err := typst.InjectValues(&markup, map[string]any{"meta": metadata, "days": days}); err != nil { + return output, err + } + + // Import the template and invoke the template function with the custom data. + // Show is used to replace the current document with whatever content the template function in `template.typ` returns. + markup.WriteString(` + #import "template.typ": abrechnung + #show: doc => abrechnung(meta, days) + `) + + // Compile the prepared markup with Typst and write the result it into `output.pdf`. + // f, err := os.Create("output.pdf") + // if err != nil { + // log.Panicf("Failed to create output file: %v.", err) + // } + // defer f.Close() + // + + typstCLI := typst.CLI{} + if err := typstCLI.Compile(&markup, &output, nil); err != nil { + return output, err + } + return output, nil +} + +func PDFFormHandler(w http.ResponseWriter, r *http.Request) { + helper.RequiresLogin(Session, w, r) + + user, err := models.GetUserFromSession(Session, r.Context()) + if err != nil { + slog.Warn("Error getting user!", slog.Any("Error", err)) + // TODO add error handling + } + + teamMembers, err := user.GetTeamMembers() + if err != nil { + slog.Warn("Error getting team members!", slog.Any("Error", err)) + } + templates.PDFForm(teamMembers).Render(r.Context(), w) +} + func PDFHandler(w http.ResponseWriter, r *http.Request) { helper.RequiresLogin(Session, w, r) startDate, err := parseTimestamp(r, "start_date", time.Now().Format("2006-01-02")) @@ -89,6 +157,25 @@ func PDFHandler(w http.ResponseWriter, r *http.Request) { aggregatedWorkTime += day.TimeWorkVirtual(user) } - // log.Printf("Using Dates: %s - %s\n", startDate.String(), endDate.String()) - templates.PDFReportEmploye(user, aggregatedOvertime, aggregatedWorkTime, weeks, startDate, endDate).Render(r.Context(), w) + typstDays, err := convertDaysToTypst(weeks, user) + if err != nil { + log.Panicf("Failed to convert days!") + } + metadata := typstMetadata{ + EmployeeName: fmt.Sprintf("%s %s", user.Vorname, user.Name), + TimeRange: fmt.Sprintf("%s - %s", startDate.Format("02.01.2006"), endDate.Format("02.01.2006")), + Overtime: helper.FormatDurationFill(aggregatedOvertime, true), + WorkTime: helper.FormatDurationFill(aggregatedWorkTime, true), + OvertimeTotal: "", + CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), + } + + output, err := renderPDF(typstDays, metadata) + if err != nil { + slog.Warn("Could not create pdf report", slog.Any("Error", err)) + } + + w.Header().Set("Content-type", "application/pdf") + output.WriteTo(w) + w.WriteHeader(200) } diff --git a/Backend/main.go b/Backend/main.go index 15d4a96..abccfd0 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -52,7 +52,8 @@ func main() { // server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) server.HandleFunc("/team", endpoints.TeamHandler) server.HandleFunc("/team/presence", endpoints.TeamPresenceHandler) - server.HandleFunc("/pdf", endpoints.PDFHandler) + server.Handle("/pdf", ParamsMiddleware(endpoints.PDFFormHandler)) + server.HandleFunc("/pdf/generate", endpoints.PDFHandler) server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect)) server.Handle("/static/", http.StripPrefix("/static/", fs)) diff --git a/Backend/static/css/styles.css b/Backend/static/css/styles.css index 8b42ab0..9098dd5 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -12,7 +12,10 @@ --color-red-700: oklch(50.5% 0.213 27.518); --color-orange-500: oklch(70.5% 0.213 47.604); --color-purple-600: oklch(55.8% 0.288 302.321); + --color-slate-300: oklch(86.9% 0.022 252.894); + --color-slate-600: oklch(44.6% 0.043 257.281); --color-slate-700: oklch(37.2% 0.044 257.287); + --color-slate-800: oklch(27.9% 0.041 260.031); --color-neutral-100: oklch(97% 0 0); --color-neutral-200: oklch(92.2% 0 0); --color-neutral-300: oklch(87% 0 0); @@ -197,6 +200,15 @@ .relative { position: relative; } + .top-1 { + top: calc(var(--spacing) * 1); + } + .top-1\/2 { + top: calc(1/2 * 100%); + } + .top-2 { + top: calc(var(--spacing) * 2); + } .top-2\.5 { top: calc(var(--spacing) * 2.5); } @@ -206,9 +218,18 @@ .right-1 { right: calc(var(--spacing) * 1); } + .right-2 { + right: calc(var(--spacing) * 2); + } .right-2\.5 { right: calc(var(--spacing) * 2.5); } + .left-1 { + left: calc(var(--spacing) * 1); + } + .left-1\/2 { + left: calc(1/2 * 100%); + } .col-span-2 { grid-column: span 2 / span 2; } @@ -236,6 +257,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 +366,9 @@ .inline { display: inline; } + .inline-flex { + display: inline-flex; + } .table { display: table; } @@ -360,6 +387,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); } @@ -378,12 +411,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 +438,9 @@ .w-full { width: 100%; } + .flex-shrink { + flex-shrink: 1; + } .flex-shrink-0 { flex-shrink: 0; } @@ -411,9 +456,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 +617,9 @@ .border-neutral-600 { border-color: var(--color-neutral-600); } + .border-slate-300 { + border-color: var(--color-slate-300); + } .bg-accent { background-color: var(--color-accent); } @@ -568,6 +641,9 @@ .bg-red-600 { background-color: var(--color-red-600); } + .mask-repeat { + mask-repeat: repeat; + } .p-1 { padding: calc(var(--spacing) * 1); } @@ -632,12 +708,32 @@ .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%; + } + .shadow { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .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 +742,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 +756,10 @@ --tw-duration: 300ms; transition-duration: 300ms; } + .select-none { + -webkit-user-select: none; + user-select: none; + } .\*\:text-center { :is(& > *) { text-align: center; @@ -711,11 +816,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) { @@ -751,6 +871,14 @@ } } } + .hover\:shadow-md { + &:hover { + @media (hover: hover) { + --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } .focus\:bg-neutral-700 { &:focus { background-color: var(--color-neutral-700); @@ -798,12 +926,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); @@ -1011,6 +1133,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 +1187,76 @@ syntax: "*"; inherits: false; } +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} @property --tw-blur { syntax: "*"; inherits: false; @@ -1090,10 +1317,33 @@ @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-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --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 0000000000000000000000000000000000000000..53694f96af0ec6289ac91689eed61ed30607890c GIT binary patch literal 17585 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 b{{ 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/pdf.templ b/Backend/templates/pdf.templ index e740c5e..c21a232 100644 --- a/Backend/templates/pdf.templ +++ b/Backend/templates/pdf.templ @@ -6,6 +6,46 @@ import ( "time" ) +templ PDFForm(teamMembers []models.User) { + @Base() + @headerComponent() +
+
+
Zeitraum wählen
+
+ + +
+
+
+
+
Mitarbeiter wählen
+
+
+ + +
+ @CheckboxComponent("pdf-123", "Kim Mustermensch") +
+
+
+
Direktvorschau oder Download
+
+} + +templ CheckboxComponent(id, label string) { +
+ +
+} + templ PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) { {{ _, kw := tsStart.ISOWeek() diff --git a/Backend/templates/pdf_templ.go b/Backend/templates/pdf_templ.go index a7ee638..05986fe 100644 --- a/Backend/templates/pdf_templ.go +++ b/Backend/templates/pdf_templ.go @@ -14,7 +14,7 @@ import ( "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,6 +35,132 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays 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, "
Zeitraum wählen
Mitarbeiter wählen
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = CheckboxComponent("pdf-123", "Kim Mustermensch").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
Direktvorschau oder Download
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func CheckboxComponent(id, label string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) _, kw := tsStart.ISOWeek() noBorder := "" @@ -42,98 +168,98 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var2 string - templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vorname) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 17, Col: 45} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var3 string - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 17, Col: 56} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

Zeitraum: ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var4 string - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(tsStart.Format("02.01.2006")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 18, Col: 52} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " - ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var5 string - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(tsEnd.Format("02.01.2006")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 18, Col: 98} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

Arbeitszeit: ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(worktime)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 19, Col: 58} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

Überstunden: ") - 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, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(kw) + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vorname) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 23, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 57, Col: 45} } _, 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

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 57, Col: 56} + } + _, 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, 10, "

Zeitraum: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(tsStart.Format("02.01.2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 58, Col: 52} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " - ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, 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: 58, Col: 98} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

Arbeitszeit: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(worktime)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 59, Col: 58} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

Überstunden: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, 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: 60, Col: 59} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(kw) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 63, Col: 52} + } + _, 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, "

Kommen

Gehen

Arbeitsart

Stunden

Pause

Überstunden

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -142,60 +268,60 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, 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...) + var templ_7745c5c3_Var15 = []any{noBorder} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">") 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")) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 76, Col: 59} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + _, 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, 11, "

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

") 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...) + var templ_7745c5c3_Var18 = []any{"grid grid-cols-subgrid col-span-3 " + noBorder} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -203,84 +329,84 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays workDay, _ := day.(*models.WorkDay) for bookingI := 0; bookingI < len(workDay.Bookings); bookingI += 2 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

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

") 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")) + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 83, Col: 64} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + _, 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, 15, "

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

") 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")) + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 84, Col: 66} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + _, 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, 16, "

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

") 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) + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 85, Col: 55} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

") + 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, 18, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if workDay.IsKurzArbeit() { timeFrom, timeTo := workDay.GenerateKurzArbeitBookings(e) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(timeFrom.Format("15:04")) + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(timeFrom.Format("15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 51, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 91, Col: 36} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, 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, 20, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(timeTo.Format("15:04")) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(timeTo.Format("15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 52, Col: 34} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 92, Col: 34} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

Kurzarbeit

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

Kurzarbeit

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -288,25 +414,25 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays } else { absentDay, _ := day.(*models.Absence) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(absentDay.AbwesenheitTyp.Name) + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, 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: 59, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 99, Col: 62} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + _, 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, 23, "

") + 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, 24, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -315,7 +441,7 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -323,7 +449,7 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -331,18 +457,18 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays 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, 34, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if day.Date().Weekday() == time.Friday { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

Wochenende

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

Wochenende

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -366,9 +492,9 @@ func ColorDuration(d time.Duration, classes string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var20 := templ.GetChildren(ctx) - if templ_7745c5c3_Var20 == nil { - templ_7745c5c3_Var20 = templ.NopComponent + templ_7745c5c3_Var26 := templ.GetChildren(ctx) + if templ_7745c5c3_Var26 == nil { + templ_7745c5c3_Var26 = templ.NopComponent } ctx = templ.ClearChildren(ctx) @@ -376,38 +502,38 @@ func ColorDuration(d time.Duration, classes string) templ.Component { if d.Abs() < time.Minute { color = "text-neutral-300" } - var templ_7745c5c3_Var21 = []any{color + " " + classes} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var21...) + var templ_7745c5c3_Var27 = []any{color + " " + classes} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var27...) 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, 38, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDurationFill(d, true)) + var templ_7745c5c3_Var29 string + templ_7745c5c3_Var29, 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: 81, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 121, Col: 72} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + _, 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, 32, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/WIR-typst/template.typ b/WIR-typst/template.typ deleted file mode 100644 index e98da26..0000000 --- a/WIR-typst/template.typ +++ /dev/null @@ -1,58 +0,0 @@ -#let table-header(..headers) = { - table.header( - ..headers.pos().map(h => strong(text(fill: white, h))) - ) -} - -#set table( - stroke: black, - inset: .5em, - align: center, -) - -#let abrechnung(meta, days) = { - set page(paper: "a4") - - [= Abrechnung Arbeitszeit -- #meta.employee-name] - - [Zeitraum: #meta.Zeitraum - - Arbeitszeit: #user.Arbeitszeit - - Überstunden: #user.Überstunden -] - - table( - columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1.25fr), - fill: (x, y) => - if y == 0 { gray }, - align: center, - table-header( - [Datum], [Kommen], [Gehen], [Arbeitsart], [Stunden], [Pause], [Überstunden] - ), - .. for day in days { - ( - [#day.Date], - table.cell(colspan: 3, inset: 0em)[ - #table( - columns: (1fr, 1fr, 1fr), - gutter: 0em, - stroke: black, - .. for Zeit in day.Zeiten { - ( - [#Zeit.Kommen], - [#Zeit.Gehen], - [#Zeit.Art], - ) - }, - ) - ], - [#day.Arbeitszeit], - [#day.Pause], - [#day.Überstunden], - - ) - - } - ) -} diff --git a/WIR-typst/test.typ b/WIR-typst/test.typ deleted file mode 100644 index 555ce61..0000000 --- a/WIR-typst/test.typ +++ /dev/null @@ -1,36 +0,0 @@ -#let user = ( - Name: "Mustermensch", - Vorname: "Kim", - Arbeitszeit: "139h 12min", - Überstunden: "-14h 12min" -) - -#let meta = ( - Zeitraum: "01.09.2025 - 30.09.2025", - KW: "26" -) - -#let days = ( - ( - Date: "01.09.2025", - Zeiten: ( - (Kommen: "07:17", Gehen: "14:13", Art: "Büro"), - (Kommen: "14:24", Gehen: "16:13", Art: "Homeoffice") - ), - Arbeitszeit: "7h 32min", - Pause: "34min", - Überstunden: "12min" - ),( - Date: "02.09.2025", - Zeiten: ( - (Kommen: "07:23", Gehen: "14:21", Art: "Büro"), - (Kommen: "14:38", Gehen: "17:13", Art: "Homeoffice") - ), - Arbeitszeit: "6h 22min", - Pause: "45min", - Überstunden: "-23min" - ) -) - -#import "template.typ": abrechnung -#show: doc => abrechnung(meta, user, days) \ No newline at end of file -- 2.49.1 From e1f0f85401eaef1d25c6dc46f519e2baa744c09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 24 Oct 2025 12:20:05 +0200 Subject: [PATCH 05/35] small refactor of sonarqube issue --- Backend/endpoints/time.go | 19 ------------------- Backend/go.mod | 10 ++++++++-- Backend/go.sum | 12 ++++++++++-- Backend/models/workDay.go | 22 +++++++++++++--------- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index 102c3f2..0ed80a8 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -261,22 +261,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/go.mod b/Backend/go.mod index 030b122..34c75a2 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -9,16 +9,22 @@ require github.com/a-h/templ v0.3.943 require github.com/alexedwards/scs/v2 v2.8.0 require ( + github.com/Dadido3/go-typst v0.3.0 github.com/golang-migrate/migrate/v4 v4.18.3 github.com/joho/godotenv v1.5.1 ) require ( - github.com/Dadido3/go-typst v0.3.0 // indirect 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 a4fe99c..4bbe15a 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -72,11 +72,19 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3 go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index 0fc67aa..de5cf14 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -39,35 +39,39 @@ type WorkDay struct { func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay { var allDays map[string]IWorkDay = make(map[string]IWorkDay) - var sortedDays []IWorkDay + for _, day := range GetWorkDays(user, tsFrom, tsTo) { allDays[day.Date().Format("2006-01-02")] = &day } absences, err := GetAbsencesByCardUID(user.CardUID, tsFrom, tsTo) if err != nil { log.Println("Error gettings absences for all Days!", err) - return sortedDays + return nil } 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 - } + if workDay, ok := allDays[day.Date().Format("2006-01-02")].(*WorkDay); ok && len(workDay.Bookings) > 0 { + workDay.kurzArbeit = true + workDay.kurzArbeitAbsence = day } } else { allDays[day.Date().Format("2006-01-02")] = &day } } - for _, day := range allDays { + sortedDays := sortDays(allDays, orderedForward) + return sortedDays +} + +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()) }) -- 2.49.1 From a634b7a69ee21036f61a27dbecc65cf436fbfe09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Mon, 27 Oct 2025 22:53:07 +0100 Subject: [PATCH 06/35] separaded endpoints + cleaned page templates + added constants to time formatting --- Backend/endpoints/pdf-create.go | 161 ++++++++ Backend/endpoints/pdf.go | 156 -------- Backend/endpoints/team.go | 2 +- Backend/endpoints/time.go | 10 +- Backend/endpoints/user-settings.go | 2 +- Backend/helper/logs/main.go | 2 +- Backend/helper/paramParser/main.go | 31 ++ Backend/helper/time_test.go | 4 +- Backend/main.go | 2 +- Backend/models/workDay.go | 8 +- Backend/models/workDay_test.go | 2 +- Backend/models/workWeek_test.go | 4 +- Backend/static/css/styles.css | 107 +----- Backend/static/script.js | 6 + Backend/templates/headerComponent.templ | 3 +- Backend/templates/headerComponent_templ.go | 4 +- Backend/templates/pages.templ | 40 +- Backend/templates/pages_templ.go | 119 +----- Backend/templates/pdf.templ | 30 +- Backend/templates/pdf_templ.go | 415 +++++++++++---------- Backend/templates/presencePage.templ | 29 ++ Backend/templates/presencePage_templ.go | 110 ++++++ Backend/templates/timeComponents.templ | 8 +- Backend/templates/timeComponents_templ.go | 16 +- Backend/templates/timePage.templ | 8 +- Backend/templates/timePage_templ.go | 14 +- 26 files changed, 654 insertions(+), 639 deletions(-) create mode 100644 Backend/endpoints/pdf-create.go create mode 100644 Backend/helper/paramParser/main.go create mode 100644 Backend/templates/presencePage.templ create mode 100644 Backend/templates/presencePage_templ.go diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go new file mode 100644 index 0000000..5184c70 --- /dev/null +++ b/Backend/endpoints/pdf-create.go @@ -0,0 +1,161 @@ +package endpoints + +import ( + "arbeitszeitmessung/helper" + "arbeitszeitmessung/helper/paramParser" + "arbeitszeitmessung/models" + "bytes" + "fmt" + "log" + "log/slog" + "net/http" + "time" + + "github.com/Dadido3/go-typst" +) + +func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) { + var typstDays []typstDay + for _, day := range days { + var thisTypstDay typstDay + work, pause, overtime := day.GetAllWorkTimesVirtual(u) + thisTypstDay.Date = day.Date().Format("02.01.2006") + thisTypstDay.Worktime = helper.FormatDurationFill(work, true) + thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) + thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true) + thisTypstDay.IsFriday = day.Date().Weekday() == time.Friday + + thisTypstDay.DayParts = convertDayToTypstDayParts(day, u) + typstDays = append(typstDays, thisTypstDay) + } + return typstDays, nil +} + +func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDayPart { + var typstDayParts []typstDayPart + if day.IsWorkDay() { + workDay, _ := day.(*models.WorkDay) + for i := 0; i < len(workDay.Bookings); i += 2 { + var typstDayPart typstDayPart + typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04") + typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04") + typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name + typstDayPart.IsWorkDay = true + typstDayParts = append(typstDayParts, typstDayPart) + } + if day.IsKurzArbeit() { + tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user) + typstDayParts = append(typstDayParts, typstDayPart{ + BookingFrom: tsFrom.Format("15:04"), + BookingTo: tsTo.Format("15:04"), + WorkType: "Kurzarbeit", + IsWorkDay: true, + }) + } + } else { + absentDay, _ := day.(*models.Absence) + typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name}) + } + return typstDayParts +} + +func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) { + var markup bytes.Buffer + var output bytes.Buffer + + if err := typst.InjectValues(&markup, map[string]any{"meta": metadata, "days": days}); err != nil { + return output, err + } + + // Import the template and invoke the template function with the custom data. + // Show is used to replace the current document with whatever content the template function in `template.typ` returns. + markup.WriteString(` + #import "template.typ": abrechnung + #show: doc => abrechnung(meta, days) + `) + + // Compile the prepared markup with Typst and write the result it into `output.pdf`. + // f, err := os.Create("output.pdf") + // if err != nil { + // log.Panicf("Failed to create output file: %v.", err) + // } + // defer f.Close() + // + + typstCLI := typst.CLI{} + if err := typstCLI.Compile(&markup, &output, nil); err != nil { + return output, err + } + return output, nil +} + +func PDFHandler(w http.ResponseWriter, r *http.Request) { + helper.RequiresLogin(Session, w, r) + pp := paramParser.New(r.URL.Query()) + startDate := pp.ParseTimestamp("start_date", time.DateOnly, time.Now()) + if startDate.Day() > 1 { + startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1)) + } + endDate := startDate.AddDate(0, 1, -1) + + user, err := models.GetUserFromSession(Session, r.Context()) + if err != nil { + log.Println("Error getting user!") + } + + //TODO: only accepted weeks + + weeks := models.GetDays(user, startDate, endDate, false) + var aggregatedOvertime, aggregatedWorkTime time.Duration + for _, day := range weeks { + aggregatedOvertime += day.TimeOvertimeReal(user) + aggregatedWorkTime += day.TimeWorkVirtual(user) + } + + typstDays, err := convertDaysToTypst(weeks, user) + if err != nil { + log.Panicf("Failed to convert days!") + } + metadata := typstMetadata{ + EmployeeName: fmt.Sprintf("%s %s", user.Vorname, user.Name), + TimeRange: fmt.Sprintf("%s - %s", startDate.Format("02.01.2006"), endDate.Format("02.01.2006")), + Overtime: helper.FormatDurationFill(aggregatedOvertime, true), + WorkTime: helper.FormatDurationFill(aggregatedWorkTime, true), + OvertimeTotal: "", + CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), + } + + output, err := renderPDF(typstDays, metadata) + if err != nil { + slog.Warn("Could not create pdf report", slog.Any("Error", err)) + } + + w.Header().Set("Content-type", "application/pdf") + output.WriteTo(w) + w.WriteHeader(200) +} + +type typstMetadata struct { + TimeRange string `json:"time-range"` + EmployeeName string `json:"employee-name"` + WorkTime string `json:"worktime"` + Overtime string `json:"overtime"` + OvertimeTotal string `json:"overtime-total"` + CurrentTimestamp string `json:"current-timestamp"` +} + +type typstDayPart struct { + BookingFrom string `json:"booking-from"` + BookingTo string `json:"booking-to"` + WorkType string `json:"worktype"` + IsWorkDay bool `json:"is-workday"` +} + +type typstDay struct { + Date string `json:"date"` + DayParts []typstDayPart `json:"day-parts"` + Worktime string `json:"worktime"` + Pausetime string `json:"pausetime"` + Overtime string `json:"overtime"` + IsFriday bool `json:"is-weekend"` +} diff --git a/Backend/endpoints/pdf.go b/Backend/endpoints/pdf.go index eb36e9f..e08ef6d 100644 --- a/Backend/endpoints/pdf.go +++ b/Backend/endpoints/pdf.go @@ -4,116 +4,10 @@ import ( "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "arbeitszeitmessung/templates" - "bytes" - "fmt" - "log" "log/slog" "net/http" - "time" - - "github.com/Dadido3/go-typst" ) -type typstMetadata struct { - TimeRange string `json:"time-range"` - EmployeeName string `json:"employee-name"` - WorkTime string `json:"worktime"` - Overtime string `json:"overtime"` - OvertimeTotal string `json:"overtime-total"` - CurrentTimestamp string `json:"current-timestamp"` -} - -type typstDayPart struct { - BookingFrom string `json:"booking-from"` - BookingTo string `json:"booking-to"` - WorkType string `json:"worktype"` - IsWorkDay bool `json:"is-workday"` -} - -type typstDay struct { - Date string `json:"date"` - DayParts []typstDayPart `json:"day-parts"` - Worktime string `json:"worktime"` - Pausetime string `json:"pausetime"` - Overtime string `json:"overtime"` - IsFriday bool `json:"is-weekend"` -} - -func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) { - var typstDays []typstDay - for _, day := range days { - var thisTypstDay typstDay - work, pause, overtime := day.GetAllWorkTimesVirtual(u) - thisTypstDay.Date = day.Date().Format("02.01.2006") - thisTypstDay.Worktime = helper.FormatDurationFill(work, true) - thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) - thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true) - thisTypstDay.IsFriday = day.Date().Weekday() == time.Friday - - thisTypstDay.DayParts = convertDayToTypstDayParts(day, u) - typstDays = append(typstDays, thisTypstDay) - } - return typstDays, nil -} - -func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDayPart { - var typstDayParts []typstDayPart - if day.IsWorkDay() { - workDay, _ := day.(*models.WorkDay) - for i := 0; i < len(workDay.Bookings); i += 2 { - var typstDayPart typstDayPart - typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04") - typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04") - typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name - typstDayPart.IsWorkDay = true - typstDayParts = append(typstDayParts, typstDayPart) - } - if day.IsKurzArbeit() { - tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user) - typstDayParts = append(typstDayParts, typstDayPart{ - BookingFrom: tsFrom.Format("15:04"), - BookingTo: tsTo.Format("15:04"), - WorkType: "Kurzarbeit", - IsWorkDay: true, - }) - } - } else { - absentDay, _ := day.(*models.Absence) - typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name}) - } - return typstDayParts -} - -func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) { - var markup bytes.Buffer - var output bytes.Buffer - - if err := typst.InjectValues(&markup, map[string]any{"meta": metadata, "days": days}); err != nil { - return output, err - } - - // Import the template and invoke the template function with the custom data. - // Show is used to replace the current document with whatever content the template function in `template.typ` returns. - markup.WriteString(` - #import "template.typ": abrechnung - #show: doc => abrechnung(meta, days) - `) - - // Compile the prepared markup with Typst and write the result it into `output.pdf`. - // f, err := os.Create("output.pdf") - // if err != nil { - // log.Panicf("Failed to create output file: %v.", err) - // } - // defer f.Close() - // - - typstCLI := typst.CLI{} - if err := typstCLI.Compile(&markup, &output, nil); err != nil { - return output, err - } - return output, nil -} - func PDFFormHandler(w http.ResponseWriter, r *http.Request) { helper.RequiresLogin(Session, w, r) @@ -129,53 +23,3 @@ func PDFFormHandler(w http.ResponseWriter, r *http.Request) { } templates.PDFForm(teamMembers).Render(r.Context(), w) } - -func PDFHandler(w http.ResponseWriter, r *http.Request) { - helper.RequiresLogin(Session, w, r) - startDate, err := parseTimestamp(r, "start_date", time.Now().Format("2006-01-02")) - if err != nil { - log.Println("Error parsing 'start_date' time", err) - http.Error(w, "Timestamp 'start_date' cannot be parsed!", http.StatusBadRequest) - return - } - if startDate.Day() > 1 { - startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1)) - } - endDate := startDate.AddDate(0, 1, -1) - - user, err := models.GetUserFromSession(Session, r.Context()) - if err != nil { - log.Println("Error getting user!") - } - - //TODO: only accepted weeks - - weeks := models.GetDays(user, startDate, endDate, false) - var aggregatedOvertime, aggregatedWorkTime time.Duration - for _, day := range weeks { - aggregatedOvertime += day.TimeOvertimeReal(user) - aggregatedWorkTime += day.TimeWorkVirtual(user) - } - - typstDays, err := convertDaysToTypst(weeks, user) - if err != nil { - log.Panicf("Failed to convert days!") - } - metadata := typstMetadata{ - EmployeeName: fmt.Sprintf("%s %s", user.Vorname, user.Name), - TimeRange: fmt.Sprintf("%s - %s", startDate.Format("02.01.2006"), endDate.Format("02.01.2006")), - Overtime: helper.FormatDurationFill(aggregatedOvertime, true), - WorkTime: helper.FormatDurationFill(aggregatedWorkTime, true), - OvertimeTotal: "", - CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), - } - - output, err := renderPDF(typstDays, metadata) - if err != nil { - slog.Warn("Could not create pdf report", slog.Any("Error", err)) - } - - w.Header().Set("Content-type", "application/pdf") - output.WriteTo(w) - w.WriteHeader(200) -} diff --git a/Backend/endpoints/team.go b/Backend/endpoints/team.go index 397d45a..467b4bf 100644 --- a/Backend/endpoints/team.go +++ b/Backend/endpoints/team.go @@ -65,7 +65,7 @@ func showWeeks(w http.ResponseWriter, r *http.Request) { submissionDate := r.URL.Query().Get("submission_date") lastSub := user.GetLastWorkWeekSubmission() if submissionDate != "" { - submissionDate, err := time.Parse("2006-01-02", submissionDate) + submissionDate, err := time.Parse(time.DateOnly, submissionDate) if err == nil { lastSub = helper.GetMonday(submissionDate) } diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index 0ed80a8..5e0f6f7 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -52,7 +52,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 } @@ -69,13 +69,13 @@ func getBookings(w http.ResponseWriter, r *http.Request) { } // TODO add config for timeoffset - tsFrom, err := parseTimestamp(r, "time_from", time.Now().AddDate(0, -1, 0).Format("2006-01-02")) + tsFrom, err := parseTimestamp(r, "time_from", time.Now().AddDate(0, -1, 0).Format(time.DateOnly)) 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")) + tsTo, err := parseTimestamp(r, "time_to", time.Now().Format(time.DateOnly)) if err != nil { log.Println("Error parsing 'to' time", err) http.Error(w, "Timestamp 'to' cannot be parsed!", http.StatusBadRequest) @@ -193,13 +193,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 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/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..0b08c2d --- /dev/null +++ b/Backend/helper/paramParser/main.go @@ -0,0 +1,31 @@ +package paramParser + +import ( + "log/slog" + "net/url" + "time" +) + +type ParamsParser struct { + urlParams url.Values +} + +func New(_urlParams url.Values) ParamsParser { + return ParamsParser{ + urlParams: _urlParams, + } +} + +func (p *ParamsParser) ParseTimestamp(key string, format string, fallback time.Time) time.Time { + paramTimestamp := p.urlParams.Get(key) + if paramTimestamp == "" { + return fallback + } + if timestamp, err := time.Parse(format, paramTimestamp); err == nil { + return timestamp + } else { + slog.Warn("Error parsing HTTP Params to timestamp", slog.Any("key", key), slog.Any("error", err)) + return fallback + } + +} diff --git a/Backend/helper/time_test.go b/Backend/helper/time_test.go index 454d4e3..e09cbd3 100644 --- a/Backend/helper/time_test.go +++ b/Backend/helper/time_test.go @@ -6,8 +6,8 @@ import ( ) 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") + notMonday, err := time.Parse(time.DateOnly, "2025-07-16") if err != nil || isMonday.Equal(notMonday) { t.Errorf("U stupid? %e", err) } diff --git a/Backend/main.go b/Backend/main.go index abccfd0..98fde52 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -51,7 +51,7 @@ func main() { // server.HandleFunc("/user/login", endpoints.LoginHandler) // server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) server.HandleFunc("/team", endpoints.TeamHandler) - server.HandleFunc("/team/presence", endpoints.TeamPresenceHandler) + server.HandleFunc("/presence", endpoints.TeamPresenceHandler) server.Handle("/pdf", ParamsMiddleware(endpoints.PDFFormHandler)) server.HandleFunc("/pdf/generate", endpoints.PDFHandler) server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect)) diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index de5cf14..be74595 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -41,7 +41,7 @@ func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay var allDays map[string]IWorkDay = make(map[string]IWorkDay) for _, day := range GetWorkDays(user, tsFrom, tsTo) { - allDays[day.Date().Format("2006-01-02")] = &day + allDays[day.Date().Format(time.DateOnly)] = &day } absences, err := GetAbsencesByCardUID(user.CardUID, tsFrom, tsTo) if err != nil { @@ -53,12 +53,12 @@ func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay continue } if day.AbwesenheitTyp.WorkTime == 1 { - if workDay, ok := allDays[day.Date().Format("2006-01-02")].(*WorkDay); ok && len(workDay.Bookings) > 0 { + if workDay, ok := allDays[day.Date().Format(time.DateOnly)].(*WorkDay); ok && len(workDay.Bookings) > 0 { workDay.kurzArbeit = true workDay.kurzArbeitAbsence = day } } else { - allDays[day.Date().Format("2006-01-02")] = &day + allDays[day.Date().Format(time.DateOnly)] = &day } } @@ -165,7 +165,7 @@ func (d *WorkDay) TimePauseReal(u User) (work, pause time.Duration) { } 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 { diff --git a/Backend/models/workDay_test.go b/Backend/models/workDay_test.go index abc3d89..5b1aef7 100644 --- a/Backend/models/workDay_test.go +++ b/Backend/models/workDay_test.go @@ -16,7 +16,7 @@ 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")), 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 9098dd5..ee0ad93 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -620,6 +620,12 @@ .border-slate-300 { border-color: var(--color-slate-300); } + .border-slate-700 { + border-color: var(--color-slate-700); + } + .border-slate-800 { + border-color: var(--color-slate-800); + } .bg-accent { background-color: var(--color-accent); } @@ -726,10 +732,6 @@ .opacity-0 { opacity: 0%; } - .shadow { - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } .outline { outline-style: var(--tw-outline-style); outline-width: 1px; @@ -871,14 +873,6 @@ } } } - .hover\:shadow-md { - &:hover { - @media (hover: hover) { - --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - } .focus\:bg-neutral-700 { &:focus { background-color: var(--color-neutral-700); @@ -941,11 +935,6 @@ grid-column: span 3 / span 3; } } - .md\:col-span-4 { - @media (width >= 48rem) { - grid-column: span 4 / span 4; - } - } .md\:mx-\[10\%\] { @media (width >= 48rem) { margin-inline: 10%; @@ -971,6 +960,11 @@ width: calc(1/2 * 100%); } } + .md\:flex-row { + @media (width >= 48rem) { + flex-direction: row; + } + } .md\:px-4 { @media (width >= 48rem) { padding-inline: calc(var(--spacing) * 4); @@ -1187,71 +1181,6 @@ syntax: "*"; inherits: false; } -@property --tw-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-shadow-color { - syntax: "*"; - inherits: false; -} -@property --tw-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-inset-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-inset-shadow-color { - syntax: "*"; - inherits: false; -} -@property --tw-inset-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-ring-color { - syntax: "*"; - inherits: false; -} -@property --tw-ring-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-inset-ring-color { - syntax: "*"; - inherits: false; -} -@property --tw-inset-ring-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-ring-inset { - syntax: "*"; - inherits: false; -} -@property --tw-ring-offset-width { - syntax: ""; - inherits: false; - initial-value: 0px; -} -@property --tw-ring-offset-color { - syntax: "*"; - inherits: false; - initial-value: #fff; -} -@property --tw-ring-offset-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} @property --tw-outline-style { syntax: "*"; inherits: false; @@ -1329,20 +1258,6 @@ --tw-border-style: solid; --tw-divide-y-reverse: 0; --tw-font-weight: initial; - --tw-shadow: 0 0 #0000; - --tw-shadow-color: initial; - --tw-shadow-alpha: 100%; - --tw-inset-shadow: 0 0 #0000; - --tw-inset-shadow-color: initial; - --tw-inset-shadow-alpha: 100%; - --tw-ring-color: initial; - --tw-ring-shadow: 0 0 #0000; - --tw-inset-ring-color: initial; - --tw-inset-ring-shadow: 0 0 #0000; - --tw-ring-inset: initial; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-offset-shadow: 0 0 #0000; --tw-outline-style: solid; --tw-blur: initial; --tw-brightness: initial; diff --git a/Backend/static/script.js b/Backend/static/script.js index 2f3b643..a47b0ce 100644 --- a/Backend/static/script.js +++ b/Backend/static/script.js @@ -79,3 +79,9 @@ 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; + } +} diff --git a/Backend/templates/headerComponent.templ b/Backend/templates/headerComponent.templ index ec2d673..53d044a 100644 --- a/Backend/templates/headerComponent.templ +++ b/Backend/templates/headerComponent.templ @@ -4,8 +4,9 @@ templ headerComponent() {
Zeitverwaltung Abrechnung + PDF if true { - Anwesenheit + Anwesenheit } Einstellungen @LogoutButton() diff --git a/Backend/templates/headerComponent_templ.go b/Backend/templates/headerComponent_templ.go index aeef6ec..b13c034 100644 --- a/Backend/templates/headerComponent_templ.go +++ b/Backend/templates/headerComponent_templ.go @@ -29,12 +29,12 @@ func headerComponent() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Zeitverwaltung Abrechnung ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Zeitverwaltung Abrechnung PDF ") if templ_7745c5c3_Err != nil { 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, "Anwesenheit ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/pages.templ b/Backend/templates/pages.templ index b2a3475..01b142f 100644 --- a/Backend/templates/pages.templ +++ b/Backend/templates/pages.templ @@ -1,7 +1,6 @@ package templates import "arbeitszeitmessung/models" -import "arbeitszeitmessung/helper" templ Base() { @@ -29,7 +28,7 @@ templ LoginPage(success bool, errorMsg string) {
} -templ UserPage(status int) { +templ SettingsPage(status int) { {{ user := ctx.Value("user").(models.User) }} @@ -89,13 +88,13 @@ templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) {
-

Eigene Abrechnung

+

Eigene Abrechnung

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

Abrechnung Mitarbeiter

+

Abrechnung Mitarbeiter

} for _, week := range weeks { @@ -104,39 +103,6 @@ templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) {
} -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..7753eaa 100644 --- a/Backend/templates/pages_templ.go +++ b/Backend/templates/pages_templ.go @@ -9,7 +9,6 @@ import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" import "arbeitszeitmessung/models" -import "arbeitszeitmessung/helper" func Base() templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { @@ -77,7 +76,7 @@ func LoginPage(success bool, errorMsg string) templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errorMsg) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 25, Col: 46} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 24, Col: 46} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -96,7 +95,7 @@ func LoginPage(success bool, errorMsg string) templ.Component { }) } -func UserPage(status int) templ.Component { +func SettingsPage(status int) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -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: 60, 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: 60, Col: 78} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -181,7 +180,7 @@ 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: 61, Col: 75} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -260,7 +259,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

Eigene Abrechnung

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

Eigene Abrechnung

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -269,7 +268,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component return templ_7745c5c3_Err } if len(weeks) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

Abrechnung Mitarbeiter

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

Abrechnung Mitarbeiter

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -288,102 +287,6 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component }) } -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_Var10 := templ.GetChildren(ctx) - if templ_7745c5c3_Var10 == nil { - 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 templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = timeGaugeComponent(helper.BoolToInt8(present)*100-1, false).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

") - 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 - }) -} - func LogoutButton() templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context @@ -400,12 +303,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_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/pdf.templ b/Backend/templates/pdf.templ index c21a232..3b4220a 100644 --- a/Backend/templates/pdf.templ +++ b/Backend/templates/pdf.templ @@ -3,6 +3,7 @@ package templates import ( "arbeitszeitmessung/helper" "arbeitszeitmessung/models" + "fmt" "time" ) @@ -10,33 +11,44 @@ templ PDFForm(teamMembers []models.User) { @Base() @headerComponent()
-
-
Zeitraum wählen
+
+

PDF Abrechnung erstellen

+
+
+
Zeitraum wählen
-
-
Mitarbeiter wählen
+
+
Mitarbeiter wählen
- - + +
- @CheckboxComponent("pdf-123", "Kim Mustermensch") + for _, member := range teamMembers { + @CheckboxComponent(fmt.Sprintf("pdf-%d", member.PersonalNummer), fmt.Sprintf("%s %s", member.Vorname, member.Name)) + }
-
Direktvorschau oder Download
+
+
Direktvorschau oder Download
+
+ + +
+
} templ CheckboxComponent(id, label string) {
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -156,9 +193,9 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var7 := templ.GetChildren(ctx) - if templ_7745c5c3_Var7 == nil { - templ_7745c5c3_Var7 = templ.NopComponent + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) @@ -168,98 +205,98 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays 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_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vorname) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 57, Col: 45} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 57, Col: 56} - } - _, 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, 10, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(tsStart.Format("02.01.2006")) + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vorname) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 58, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 69, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " - ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(tsEnd.Format("02.01.2006")) + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 58, Col: 98} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 69, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

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

Zeitraum: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(worktime)) + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(tsStart.Format("02.01.2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 59, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 70, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

Überstunden: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " - ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(overtime)) + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(tsEnd.Format("02.01.2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 60, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 70, Col: 98} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

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

Arbeitszeit: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(kw) + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(worktime)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 63, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 71, Col: 58} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

Kommen

Gehen

Arbeitsart

Stunden

Pause

Überstunden

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

Überstunden: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(overtime)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 72, Col: 59} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "

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

Kommen

Gehen

Arbeitsart

Stunden

Pause

Überstunden

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -268,60 +305,60 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays if index == len(workDays)-1 { noBorder = "border-b-0" } - var templ_7745c5c3_Var15 = []any{noBorder} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) + var templ_7745c5c3_Var17 = []any{noBorder} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, 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: 76, Col: 59} - } - _, 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, 18, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var18 = []any{"grid grid-cols-subgrid col-span-3 " + noBorder} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...) - 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 } var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var18).String()) + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 1, Col: 0} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 88, Col: 59} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 = []any{"grid grid-cols-subgrid col-span-3 " + noBorder} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -329,65 +366,27 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays workDay, _ := day.(*models.WorkDay) for bookingI := 0; bookingI < len(workDay.Bookings); bookingI += 2 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, 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: 83, Col: 64} - } - _, 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, 22, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, 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: 84, Col: 66} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI].BookingType.Name) + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI].Timestamp.Format("15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 85, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 95, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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 - } - if workDay.IsKurzArbeit() { - - timeFrom, timeTo := workDay.GenerateKurzArbeitBookings(e) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(timeFrom.Format("15:04")) + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI+1].Timestamp.Format("15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 91, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 96, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { @@ -398,15 +397,53 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays return templ_7745c5c3_Err } var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(timeTo.Format("15:04")) + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.Bookings[bookingI].BookingType.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 92, Col: 34} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 97, Col: 55} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

Kurzarbeit

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

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if workDay.IsKurzArbeit() { + + timeFrom, timeTo := workDay.GenerateKurzArbeitBookings(e) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(timeFrom.Format("15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 103, Col: 36} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(timeTo.Format("15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 104, Col: 34} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "

Kurzarbeit

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -414,25 +451,25 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays } else { absentDay, _ := day.(*models.Absence) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(absentDay.AbwesenheitTyp.Name) + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(absentDay.AbwesenheitTyp.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 99, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 111, Col: 62} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) + _, 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, 30, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -441,7 +478,7 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -449,7 +486,7 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -457,18 +494,18 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if day.Date().Weekday() == time.Friday { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "

Wochenende

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

Wochenende

") 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, 40, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -492,9 +529,9 @@ func ColorDuration(d time.Duration, classes string) 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) @@ -502,38 +539,38 @@ func ColorDuration(d time.Duration, classes string) templ.Component { if d.Abs() < time.Minute { color = "text-neutral-300" } - var templ_7745c5c3_Var27 = []any{color + " " + classes} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var27...) + var templ_7745c5c3_Var29 = []any{color + " " + classes} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDurationFill(d, true)) + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDurationFill(d, true)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 121, 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_Var29)) + _, 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, 39, "

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

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

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/timeComponents.templ b/Backend/templates/timeComponents.templ index 9730101..9fad41d 100644 --- a/Backend/templates/timeComponents.templ +++ b/Backend/templates/timeComponents.templ @@ -77,8 +77,8 @@ templ absenceComponent(a *models.Absence, isKurzarbeit bool) { } }}
- - + +

@@ -89,7 +89,7 @@ templ absenceComponent(a *models.Absence, isKurzarbeit bool) {

if isKurzarbeit { - + }
} @@ -97,7 +97,7 @@ templ absenceComponent(a *models.Absence, isKurzarbeit bool) { templ newBookingComponent(d *models.WorkDay) { diff --git a/Backend/templates/pdf_templ.go b/Backend/templates/pdf_templ.go index 3af3360..a15eca1 100644 --- a/Backend/templates/pdf_templ.go +++ b/Backend/templates/pdf_templ.go @@ -88,7 +88,7 @@ func PDFForm(teamMembers []models.User) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
Direktvorschau oder Download
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
PDFs Bündeln
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } -- 2.49.1 From ac59d2642fac593c0191aca5d92b9ba31269319b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Wed, 29 Oct 2025 00:17:07 +0100 Subject: [PATCH 09/35] fixed sonarqube issues --- Backend/endpoints/pdf-create.go | 8 +++++--- Backend/helper/paramParser/main.go | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index 335672d..17db0db 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -14,12 +14,14 @@ import ( "github.com/Dadido3/go-typst" ) +const DE_DATE string = "02.01.2006" + func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) { var typstDays []typstDay for _, day := range days { var thisTypstDay typstDay work, pause, overtime := day.GetAllWorkTimesVirtual(u) - thisTypstDay.Date = day.Date().Format("02.01.2006") + thisTypstDay.Date = day.Date().Format(DE_DATE) thisTypstDay.Worktime = helper.FormatDurationFill(work, true) thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true) @@ -140,7 +142,7 @@ func createEmployeReport(employee models.User, startDate, endDate time.Time) (by metadata := typstMetadata{ EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name), - TimeRange: fmt.Sprintf("%s - %s", startDate.Format("02.01.2006"), endDate.Format("02.01.2006")), + TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)), Overtime: helper.FormatDurationFill(worktimeBalance, true), WorkTime: helper.FormatDurationFill(actualHours, true), OvertimeTotal: "", @@ -179,7 +181,7 @@ func PDFHandler(w http.ResponseWriter, r *http.Request) { } metadata := typstMetadata{ EmployeeName: fmt.Sprintf("%s %s", user.Vorname, user.Name), - TimeRange: fmt.Sprintf("%s - %s", startDate.Format("02.01.2006"), endDate.Format("02.01.2006")), + TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)), Overtime: helper.FormatDurationFill(aggregatedOvertime, true), WorkTime: helper.FormatDurationFill(aggregatedWorkTime, true), OvertimeTotal: "", diff --git a/Backend/helper/paramParser/main.go b/Backend/helper/paramParser/main.go index 472ed58..be046a8 100644 --- a/Backend/helper/paramParser/main.go +++ b/Backend/helper/paramParser/main.go @@ -20,9 +20,9 @@ func (e *NoValueError) Error() string { return fmt.Sprintf("No value found for key %s", e.Key) } -func New(_urlParams url.Values) ParamsParser { +func New(params url.Values) ParamsParser { return ParamsParser{ - urlParams: _urlParams, + urlParams: params, } } -- 2.49.1 From 7e5eaebca91ba4bbf156b4882ed16012378b52fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 31 Oct 2025 23:57:42 +0100 Subject: [PATCH 10/35] added worktime base to work time calculations --- Backend/endpoints/pdf-create.go | 4 +- Backend/helper/time.go | 1 + Backend/models/absence.go | 49 +++++++++++++ Backend/models/workDay.go | 119 +++++++++++++++++++++++++++++++- 4 files changed, 171 insertions(+), 2 deletions(-) diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index 17db0db..cfeab6a 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -20,7 +20,7 @@ func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, erro var typstDays []typstDay for _, day := range days { var thisTypstDay typstDay - work, pause, overtime := day.GetAllWorkTimesVirtual(u) + work, pause, overtime := day.GetTimesVirtual(u, models.WorktimeBaseWeek) thisTypstDay.Date = day.Date().Format(DE_DATE) thisTypstDay.Worktime = helper.FormatDurationFill(work, true) thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) @@ -129,6 +129,8 @@ func createEmployeReport(employee models.User, startDate, endDate time.Time) (by targetHours := (employee.ArbeitszeitProWoche() / 5) * time.Duration(helper.GetWorkingDays(startDate, endDate)) workingDays := models.GetDays(employee, startDate, endDate, false) + slog.Debug("Baseline Working hours", "targetHours", targetHours.Hours()) + var actualHours time.Duration for _, day := range workingDays { actualHours += day.TimeWorkVirtual(employee) diff --git a/Backend/helper/time.go b/Backend/helper/time.go index 30198fc..f55a92c 100644 --- a/Backend/helper/time.go +++ b/Backend/helper/time.go @@ -31,6 +31,7 @@ func FormatDuration(d time.Duration) string { // Converts duration to string func FormatDurationFill(d time.Duration, fill bool) string { + return fmt.Sprintf("%.1f", d.Hours()) hours := int(d.Abs().Hours()) minutes := int(d.Abs().Minutes()) % 60 sign := "" diff --git a/Backend/models/absence.go b/Backend/models/absence.go index e3296a4..0f99bbb 100644 --- a/Backend/models/absence.go +++ b/Backend/models/absence.go @@ -49,6 +49,55 @@ func (a *Absence) IsMultiDay() bool { return !a.DateFrom.Equal(a.DateTo) } +func (a *Absence) GetWorktimeReal(u User, base WorktimeBase) time.Duration { + if a.AbwesenheitTyp.WorkTime <= 1 { + return 0 + } + switch base { + case WorktimeBaseDay: + return u.ArbeitszeitProTag() + case WorktimeBaseWeek: + return u.ArbeitszeitProWoche() / 5 + } + return 0 +} +func (a *Absence) GetPausetimeReal(u User, base WorktimeBase) time.Duration { + return 0 +} + +func (a *Absence) GetOvertimeReal(u User, base WorktimeBase) time.Duration { + if a.AbwesenheitTyp.WorkTime > 1 { + return 0 + } + switch base { + case WorktimeBaseDay: + return -u.ArbeitszeitProTag() + case WorktimeBaseWeek: + return -u.ArbeitszeitProWoche() / 5 + } + return 0 +} + +func (a *Absence) GetWorktimeVirtual(u User, base WorktimeBase) time.Duration { + return a.GetWorktimeReal(u, base) +} + +func (a *Absence) GetPausetimeVirtual(u User, base WorktimeBase) time.Duration { + return a.GetPausetimeReal(u, base) +} + +func (a *Absence) GetOvertimeVirtual(u User, base WorktimeBase) time.Duration { + return a.GetOvertimeReal(u, base) +} + +func (a *Absence) GetTimesReal(u User, base WorktimeBase) (work, pause, overtime time.Duration) { + return a.GetWorktimeReal(u, base), a.GetPausetimeReal(u, base), a.GetOvertimeReal(u, base) +} + +func (a *Absence) GetTimesVirtual(u User, base WorktimeBase) (work, pause, overtime time.Duration) { + return a.GetWorktimeVirtual(u, base), a.GetPausetimeVirtual(u, base), a.GetOvertimeVirtual(u, base) +} + func (a *Absence) TimeWorkVirtual(u User) time.Duration { return a.TimeWorkReal(u) } diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index be74595..b7d3254 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -22,6 +22,14 @@ type IWorkDay interface { IsKurzArbeit() bool GetDayProgress(User) int8 RequiresAction() bool + GetWorktimeReal(User, WorktimeBase) time.Duration + GetPausetimeReal(User, WorktimeBase) time.Duration + GetOvertimeReal(User, WorktimeBase) time.Duration + GetWorktimeVirtual(User, WorktimeBase) time.Duration + GetPausetimeVirtual(User, WorktimeBase) time.Duration + GetOvertimeVirtual(User, WorktimeBase) time.Duration + GetTimesReal(User, WorktimeBase) (work, pause, overtime time.Duration) + GetTimesVirtual(User, WorktimeBase) (work, pause, overtime time.Duration) } type WorkDay struct { @@ -37,6 +45,13 @@ type WorkDay struct { kurzArbeitAbsence Absence } +type WorktimeBase string + +const ( + WorktimeBaseWeek WorktimeBase = "week" + WorktimeBaseDay WorktimeBase = "day" +) + func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay { var allDays map[string]IWorkDay = make(map[string]IWorkDay) @@ -66,6 +81,108 @@ func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay return sortedDays } +// Gets the time as is in the db (with corrected pause times) +func (d *WorkDay) GetWorktimeReal(u User, base WorktimeBase) time.Duration { + work, pause := calcWorkPause(d.Bookings) + work, pause = correctWorkPause(work, pause) + return work +} + +// Gets the corrected pause times based on db entries +func (d *WorkDay) GetPausetimeReal(u User, base WorktimeBase) time.Duration { + work, pause := calcWorkPause(d.Bookings) + work, pause = correctWorkPause(work, pause) + return pause +} + +// Returns the overtime based on the db entries +func (d *WorkDay) GetOvertimeReal(u User, base WorktimeBase) time.Duration { + work, pause := calcWorkPause(d.Bookings) + work, pause = correctWorkPause(work, pause) + + var targetHours time.Duration + switch base { + case WorktimeBaseDay: + targetHours = u.ArbeitszeitProTag() + case WorktimeBaseWeek: + targetHours = u.ArbeitszeitProWoche() / 5 + } + return work - targetHours +} + +// Returns the worktime based on absence or kurzarbeit +func (d *WorkDay) GetWorktimeVirtual(u User, base WorktimeBase) time.Duration { + if !d.IsKurzArbeit() { + return d.GetWorktimeReal(u, base) + } + switch base { + case WorktimeBaseDay: + return u.ArbeitszeitProTag() + case WorktimeBaseWeek: + return u.ArbeitszeitProWoche() / 5 + } + return 0 +} + +func (d *WorkDay) GetPausetimeVirtual(u User, base WorktimeBase) time.Duration { + return d.GetPausetimeReal(u, base) +} + +func (d *WorkDay) GetOvertimeVirtual(u User, base WorktimeBase) time.Duration { + work := d.GetWorktimeVirtual(u, base) + + var targetHours time.Duration + switch base { + case WorktimeBaseDay: + targetHours = u.ArbeitszeitProTag() + case WorktimeBaseWeek: + targetHours = u.ArbeitszeitProWoche() / 5 + } + return work - targetHours +} + +func (d *WorkDay) GetTimesReal(u User, base WorktimeBase) (work, pause, overtime time.Duration) { + return d.GetWorktimeReal(u, base), d.GetPausetimeReal(u, base), d.GetOvertimeReal(u, base) +} + +func (d *WorkDay) GetTimesVirtual(u User, base WorktimeBase) (work, pause, overtime time.Duration) { + return d.GetWorktimeVirtual(u, base), d.GetPausetimeVirtual(u, base), d.GetOvertimeVirtual(u, base) +} + +func calcWorkPause(bookings []Booking) (work, pause time.Duration) { + var lastBooking Booking + for _, b := range bookings { + if b.CheckInOut%2 == 1 { + if !lastBooking.Timestamp.IsZero() { + pause += b.Timestamp.Sub(lastBooking.Timestamp) + } + } else { + work += b.Timestamp.Sub(lastBooking.Timestamp) + } + lastBooking = b + } + if len(bookings)%2 == 1 { + work += time.Since(lastBooking.Timestamp.Local()) + } + return work, pause +} + +func correctWorkPause(workIn, pauseIn time.Duration) (work, pause time.Duration) { + if workIn <= 6*time.Hour || pauseIn > 45*time.Minute { + return workIn, pauseIn + } + + var diff time.Duration + if workIn <= (9*time.Hour) && pauseIn < 30*time.Minute { + diff = 30*time.Minute - pauseIn + } else if pauseIn < 45*time.Minute { + diff = 45*time.Minute - pauseIn + } + work = workIn - diff + pause = pauseIn + diff + return work, pause +} + func sortDays(days map[string]IWorkDay, forward bool) []IWorkDay { var sortedDays []IWorkDay for _, day := range days { @@ -127,7 +244,7 @@ func (d *WorkDay) TimeWorkReal(u User) time.Duration { if helper.IsSameDate(d.Date(), time.Now()) && len(d.Bookings)%2 == 1 { d.realWorkTime += time.Since(lastBooking.Timestamp.Local()) } - slog.Debug("Calculated RealWorkTime for user", "user", u, slog.String("worktime", d.realWorkTime.String())) + // slog.Debug("Calculated RealWorkTime for user", "user", u, slog.String("worktime", d.realWorkTime.String())) return d.realWorkTime } -- 2.49.1 From 6ab48eb534d38be1496ec942bee0ecbeb4e3b04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Sun, 30 Nov 2025 21:25:47 +0100 Subject: [PATCH 11/35] dev/install (#58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit improved ci/cd pipeline + interactive install script Reviewed-on: https://git.letsstein.de/tom/arbeitszeitmessung/pulls/58 Co-authored-by: Tom Tröger Co-committed-by: Tom Tröger --- .gitea/workflows/build.yaml | 3 +- .gitea/workflows/test.yaml | 4 +- Backend/database.go | 3 +- Backend/helper/time.go | 1 - Docker/.env.example | 11 ---- Docker/docker-compose.yml | 4 +- Docker/env.example | 10 ++++ Makefile | 8 +++ install.sh | 107 ++++++++++++++++++++++++++++++++++++ 9 files changed, 132 insertions(+), 19 deletions(-) delete mode 100644 Docker/.env.example create mode 100644 Docker/env.example create mode 100755 install.sh diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index c32282f..a73ba14 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -2,7 +2,8 @@ name: Arbeitszeitmessung Deploy run-name: ${{ gitea.actor }} is building and deploying arbeitszeitmesssung on: push: - tags: "*" + tags: + - "*" jobs: testing: 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/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/helper/time.go b/Backend/helper/time.go index f55a92c..30198fc 100644 --- a/Backend/helper/time.go +++ b/Backend/helper/time.go @@ -31,7 +31,6 @@ func FormatDuration(d time.Duration) string { // Converts duration to string func FormatDurationFill(d time.Duration, fill bool) string { - return fmt.Sprintf("%.1f", d.Hours()) hours := int(d.Abs().Hours()) minutes := int(d.Abs().Minutes()) % 60 sign := "" 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.yml b/Docker/docker-compose.yml index f08a18b..d3d97c0 100644 --- a/Docker/docker-compose.yml +++ b/Docker/docker-compose.yml @@ -6,9 +6,7 @@ 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 diff --git a/Docker/env.example b/Docker/env.example new file mode 100644 index 0000000..b79f985 --- /dev/null +++ b/Docker/env.example @@ -0,0 +1,10 @@ +POSTGRES_USER=root # Postgres ADMIN Nutzername +POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort +POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung) +POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung) +POSTGRES_PATH=../DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...) +POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name +EXPOSED_PORT=8000 # Port auf welchem Arbeitszeitmessung läuft regex:^[0-9]{1,5}$ +TZ=Europe/Berlin # Zeitzone +PGTZ=Europe/Berlin # Zeitzone +API_TOKEN=dont_access # API Token für ESP Endpoints diff --git a/Makefile b/Makefile index 58d90fb..8de2724 100644 --- a/Makefile +++ b/Makefile @@ -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/install.sh b/install.sh new file mode 100755 index 0000000..5a0758e --- /dev/null +++ b/install.sh @@ -0,0 +1,107 @@ +#!/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 + docker compose up -d + echo "Containers started." +else + echo "You can start them manually with: docker compose up -d" +fi -- 2.49.1 From 02b5d88d34f0edba90f86d1a71db59b9e35202ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Tue, 2 Dec 2025 16:50:37 +0100 Subject: [PATCH 12/35] added typst as doc creator and put it into compose container --- Backend/endpoints/pdf-create.go | 7 +- Backend/go.mod | 2 +- Backend/go.sum | 4 +- Docker/docker-compose.dev.yml | 19 ----- Docker/docker-compose.yml | 13 +++- DocumentCreator/Dockerfile | 6 ++ DocumentCreator/static/logo.png | Bin 0 -> 17585 bytes DocumentCreator/templates/abrechnung.typ | 92 +++++++++++++++++++++++ 8 files changed, 115 insertions(+), 28 deletions(-) create mode 100644 DocumentCreator/Dockerfile create mode 100644 DocumentCreator/static/logo.png create mode 100644 DocumentCreator/templates/abrechnung.typ diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index cfeab6a..2672371 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -72,7 +72,7 @@ func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) { // 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 "template.typ": abrechnung + #import "templates/abrechnung.typ": abrechnung #show: doc => abrechnung(meta, days) `) @@ -84,7 +84,10 @@ func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) { // defer f.Close() // - typstCLI := typst.CLI{} + // typstCLI := typst.CLI{} + typstCLI := typst.DockerExec{ + ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"), + } if err := typstCLI.Compile(&markup, &output, nil); err != nil { return output, err } diff --git a/Backend/go.mod b/Backend/go.mod index 34c75a2..518cfc4 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -9,7 +9,7 @@ require github.com/a-h/templ v0.3.943 require github.com/alexedwards/scs/v2 v2.8.0 require ( - github.com/Dadido3/go-typst v0.3.0 + github.com/Dadido3/go-typst v0.8.0 github.com/golang-migrate/migrate/v4 v4.18.3 github.com/joho/godotenv v1.5.1 ) diff --git a/Backend/go.sum b/Backend/go.sum index 4bbe15a..95a0e66 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -1,7 +1,7 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Dadido3/go-typst v0.3.0 h1:Itix2FtQgBiOuHUNqgGUAK11Oo2WMlZGGGpCiQNK1IA= -github.com/Dadido3/go-typst v0.3.0/go.mod h1:QYis9sT70u65kn1SkFfyPRmHsPxgoxWbAixwfPReOZA= +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= 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 d3d97c0..a5fc78d 100644 --- a/Docker/docker-compose.yml +++ b/Docker/docker-compose.yml @@ -12,20 +12,25 @@ services: - ${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/DocumentCreator/Dockerfile b/DocumentCreator/Dockerfile new file mode 100644 index 0000000..1989fed --- /dev/null +++ b/DocumentCreator/Dockerfile @@ -0,0 +1,6 @@ +FROM ghcr.io/typst/typst:0.14.0 + +COPY ./templates ./templates +COPY ./static ./static + +ENTRYPOINT ["sh", "-c", "while true; do sleep 3600; done"] diff --git a/DocumentCreator/static/logo.png b/DocumentCreator/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..53694f96af0ec6289ac91689eed61ed30607890c GIT binary patch literal 17585 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 b{{ 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), + +) +} -- 2.49.1 From 5a5e776e8b00c8262c5e83d9a50353e5aa8d32f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Tue, 2 Dec 2025 16:52:10 +0100 Subject: [PATCH 13/35] fixed #59 --- Docker/env.example | 22 ++++++++++++---------- install.sh | 1 + 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Docker/env.example b/Docker/env.example index b79f985..ce59b7b 100644 --- a/Docker/env.example +++ b/Docker/env.example @@ -1,10 +1,12 @@ -POSTGRES_USER=root # Postgres ADMIN Nutzername -POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort -POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung) -POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung) -POSTGRES_PATH=../DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...) -POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name -EXPOSED_PORT=8000 # Port auf welchem Arbeitszeitmessung läuft regex:^[0-9]{1,5}$ -TZ=Europe/Berlin # Zeitzone -PGTZ=Europe/Berlin # Zeitzone -API_TOKEN=dont_access # API Token für ESP Endpoints +POSTGRES_USER=root # Postgres ADMIN Nutzername +POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort +POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung) +POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung) +POSTGRES_PATH=../DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...) +POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name +POSTGRES_PORT=127.0.0.1:5432 # Postgres Port will not be exposed by default. +TZ=Europe/Berlin # Zeitzone +PGTZ=Europe/Berlin # Zeitzone +API_TOKEN=dont_access # API Token für ESP Endpoints +WEB_PORT=8000 # Port from which Arbeitszeitmessung should be accessable regex:^[0-9]{1,5}$ +TYPST_CONTAINER=arbeitszeitmessung-doc-creator # Name of the pdf compiler container diff --git a/install.sh b/install.sh index 5a0758e..114c7b5 100755 --- a/install.sh +++ b/install.sh @@ -100,6 +100,7 @@ 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 -- 2.49.1 From 74bce88cc0ce875dc4a1adbd1a5c438534c087cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Tue, 2 Dec 2025 16:53:43 +0100 Subject: [PATCH 14/35] fixed #60 --- Backend/helper/time.go | 7 +++++ Backend/helper/time_test.go | 32 ++++++++++++++++++++--- Backend/templates/teamComponents.templ | 2 +- Backend/templates/teamComponents_templ.go | 6 ++--- Backend/templates/timePage.templ | 2 +- Backend/templates/timePage_templ.go | 6 ++--- 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/Backend/helper/time.go b/Backend/helper/time.go index 30198fc..fcd739d 100644 --- a/Backend/helper/time.go +++ b/Backend/helper/time.go @@ -68,3 +68,10 @@ func GetWorkingDays(startDate, endDate time.Time) int { } 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 8e56834..96ae5a9 100644 --- a/Backend/helper/time_test.go +++ b/Backend/helper/time_test.go @@ -18,7 +18,7 @@ func TestGetMonday(t *testing.T) { } func TestFormatDuration(t *testing.T) { - durations := []struct { + testCases := []struct { name string duration time.Duration }{ @@ -28,9 +28,9 @@ func TestFormatDuration(t *testing.T) { {"-1h 30min", time.Duration(-90 * time.Minute)}, {"", 0}, } - for _, d := range durations { - t.Run(d.name, func(t *testing.T) { - if FormatDuration(d.duration) != d.name { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if FormatDuration(tc.duration) != tc.name { t.Error("Format missmatch in Formatduration.") } }) @@ -58,3 +58,27 @@ func TestGetWorkingDays(t *testing.T) { }) } } + +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!") + } + }) + } +} diff --git a/Backend/templates/teamComponents.templ b/Backend/templates/teamComponents.templ index d1dd820..086bd8d 100644 --- a/Backend/templates/teamComponents.templ +++ b/Backend/templates/teamComponents.templ @@ -32,7 +32,7 @@ templ defaultWeekDayComponent(u models.User, day models.IWorkDay) {
@timeGaugeComponent(day.GetDayProgress(u), false)
-

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

+

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

if day.IsWorkDay() { {{ workDay, _ := day.(*models.WorkDay) diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index 1c0e46b..6314bce 100644 --- a/Backend/templates/teamComponents_templ.go +++ b/Backend/templates/teamComponents_templ.go @@ -151,9 +151,9 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component return templ_7745c5c3_Err } var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("Mon")) + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatGermanDayOfWeek(day.Date())) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 92} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 108} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -166,7 +166,7 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 152} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { diff --git a/Backend/templates/timePage.templ b/Backend/templates/timePage.templ index 1643cc8..88cd3ec 100644 --- a/Backend/templates/timePage.templ +++ b/Backend/templates/timePage.templ @@ -98,7 +98,7 @@ 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() { {{ diff --git a/Backend/templates/timePage_templ.go b/Backend/templates/timePage_templ.go index 0ac75a5..209fa28 100644 --- a/Backend/templates/timePage_templ.go +++ b/Backend/templates/timePage_templ.go @@ -269,9 +269,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 +284,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 { -- 2.49.1 From 6e238c45326398e9c590c1fa1baaf72ce191387e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Tue, 2 Dec 2025 16:57:10 +0100 Subject: [PATCH 15/35] update badge --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 -- 2.49.1 From 6c0a8bca640f94a5e584a2b6ae33c3600909eaa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Tue, 2 Dec 2025 17:10:17 +0100 Subject: [PATCH 16/35] added tests --- .gitignore | 1 + Backend/helper/time_test.go | 38 ++++++++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) 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/helper/time_test.go b/Backend/helper/time_test.go index 96ae5a9..01b3dd8 100644 --- a/Backend/helper/time_test.go +++ b/Backend/helper/time_test.go @@ -8,13 +8,22 @@ import ( func TestGetMonday(t *testing.T) { 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) { @@ -26,17 +35,40 @@ func TestFormatDuration(t *testing.T) { {"30min", time.Duration(30 * time.Minute)}, {"1h 30min", time.Duration(90 * time.Minute)}, {"-1h 30min", time.Duration(-90 * time.Minute)}, - {"", 0}, + {"0min", 0}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - if FormatDuration(tc.duration) != tc.name { + if FormatDurationFill(tc.duration, true) != tc.name { t.Error("Format missmatch in Formatduration.") } }) } } +func TestIsSameDate(t *testing.T) { + testCases := []struct { + dateA string + dateB string + result bool + }{ + {"2025-12-01 00:00:00", "2025-12-01 00:00:00", true}, + {"2025-12-03 00:00:00", "2025-12-02 00:00:00", false}, + {"2025-12-03 23:45:00", "2025-12-03 00:00:00", true}, + {"2025-12-04 24:12:00", "2025-12-04 00:12:00", false}, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("IsSameDateTest: %s date", tc.dateA), func(t *testing.T) { + dateA, _ := time.Parse(time.DateTime, tc.dateA) + dateB, _ := time.Parse(time.DateTime, tc.dateB) + if IsSameDate(dateA, dateB) != tc.result { + t.Errorf("Is SameDate did not match! Result %t", IsSameDate(dateA, dateB)) + } + }) + } +} + func TestGetWorkingDays(t *testing.T) { testCases := []struct { start string -- 2.49.1 From 386f11ec7e493550a99fef14f4bdb7b795196042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Wed, 3 Dec 2025 00:20:11 +0100 Subject: [PATCH 17/35] fixed #63 all scheduled tasks are now under route /auto and will require api key in the future --- Backend/endpoints/auto-kurzarbeit.go | 82 +++++++++++++++++++ .../{auto_logout.go => auto-logout.go} | 2 +- Backend/endpoints/pdf-create.go | 37 ++++----- Backend/helper/paramParser/main.go | 2 +- Backend/helper/time_test.go | 14 ++-- Backend/main.go | 3 +- Backend/models/user.go | 28 +++++++ DB/initdb/01_schema.sql | 2 +- DocumentCreator/templates/abrechnung.typ | 2 +- 9 files changed, 138 insertions(+), 34 deletions(-) create mode 100644 Backend/endpoints/auto-kurzarbeit.go rename Backend/endpoints/{auto_logout.go => auto-logout.go} (95%) diff --git a/Backend/endpoints/auto-kurzarbeit.go b/Backend/endpoints/auto-kurzarbeit.go new file mode 100644 index 0000000..3a1e394 --- /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.GetWorktimeReal(user, models.WorktimeBaseDay) >= day.GetWorktimeVirtual(user, models.WorktimeBaseDay) { + continue + } + + worktimeKurzarbeit := day.GetWorktimeVirtual(user, models.WorktimeBaseDay) - day.GetWorktimeReal(user, models.WorktimeBaseDay) + + 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 95% rename from Backend/endpoints/auto_logout.go rename to Backend/endpoints/auto-logout.go index 7d6de92..2f38541 100644 --- a/Backend/endpoints/auto_logout.go +++ b/Backend/endpoints/auto-logout.go @@ -20,7 +20,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) { } func autoLogout(w http.ResponseWriter) { - users, err := (*models.User).GetAll(nil) + users, err := models.GetAllUsers() var logged_out_users []models.User if err != nil { fmt.Printf("Error getting user list %v\n", err) diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index 2672371..7d1ae45 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -20,7 +20,7 @@ func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, erro var typstDays []typstDay for _, day := range days { var thisTypstDay typstDay - work, pause, overtime := day.GetTimesVirtual(u, models.WorktimeBaseWeek) + work, pause, overtime := day.GetTimesReal(u, models.WorktimeBaseWeek) thisTypstDay.Date = day.Date().Format(DE_DATE) thisTypstDay.Worktime = helper.FormatDurationFill(work, true) thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) @@ -45,15 +45,6 @@ func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDay typstDayPart.IsWorkDay = true typstDayParts = append(typstDayParts, typstDayPart) } - if day.IsKurzArbeit() { - tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user) - typstDayParts = append(typstDayParts, typstDayPart{ - BookingFrom: tsFrom.Format("15:04"), - BookingTo: tsTo.Format("15:04"), - WorkType: "Kurzarbeit", - IsWorkDay: true, - }) - } } else { absentDay, _ := day.(*models.Absence) typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name}) @@ -61,11 +52,11 @@ func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDay return typstDayParts } -func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) { +func renderPDF(data []typstData) (bytes.Buffer, error) { var markup bytes.Buffer var output bytes.Buffer - if err := typst.InjectValues(&markup, map[string]any{"meta": metadata, "days": days}); err != nil { + if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil { return output, err } @@ -73,18 +64,13 @@ func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) { // Show is used to replace the current document with whatever content the template function in `template.typ` returns. markup.WriteString(` #import "templates/abrechnung.typ": abrechnung - #show: doc => abrechnung(meta, days) + #for d in data { + abrechnung(d.Meta, d.Days) + } `) // Compile the prepared markup with Typst and write the result it into `output.pdf`. - // f, err := os.Create("output.pdf") - // if err != nil { - // log.Panicf("Failed to create output file: %v.", err) - // } - // defer f.Close() - // - // typstCLI := typst.CLI{} typstCLI := typst.DockerExec{ ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"), } @@ -113,7 +99,7 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-type", "application/pdf") output.WriteTo(w) - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) default: http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) @@ -154,7 +140,7 @@ func createEmployeReport(employee models.User, startDate, endDate time.Time) (by CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), } - return renderPDF(typstDays, metadata) + return renderPDF([]typstData{{Meta: metadata, Days: typstDays}, {Meta: metadata, Days: typstDays}}) } func PDFHandler(w http.ResponseWriter, r *http.Request) { @@ -193,7 +179,7 @@ func PDFHandler(w http.ResponseWriter, r *http.Request) { CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), } - output, err := renderPDF(typstDays, metadata) + output, err := renderPDF([]typstData{{Meta: metadata, Days: typstDays}}) if err != nil { slog.Warn("Could not create pdf report", slog.Any("Error", err)) } @@ -227,3 +213,8 @@ type typstDay struct { Overtime string `json:"overtime"` IsFriday bool `json:"is-weekend"` } + +type typstData struct { + Meta typstMetadata `json:"meta"` + Days []typstDay `json:"days"` +} diff --git a/Backend/helper/paramParser/main.go b/Backend/helper/paramParser/main.go index be046a8..828b01f 100644 --- a/Backend/helper/paramParser/main.go +++ b/Backend/helper/paramParser/main.go @@ -59,7 +59,7 @@ func (p *ParamsParser) ParseStringFallback(key string, fallback string) string { return p.urlParams.Get(key) } -func (p *ParamsParser) ParseString(key string, fallback string) (string, error) { +func (p *ParamsParser) ParseString(key string) (string, error) { if !p.urlParams.Has(key) { return "", &NoValueError{Key: key} } diff --git a/Backend/helper/time_test.go b/Backend/helper/time_test.go index 01b3dd8..bb10294 100644 --- a/Backend/helper/time_test.go +++ b/Backend/helper/time_test.go @@ -30,16 +30,18 @@ func TestFormatDuration(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)}, - {"0min", 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 _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - if FormatDurationFill(tc.duration, true) != tc.name { + if FormatDurationFill(tc.duration, tc.fill) != tc.name { t.Error("Format missmatch in Formatduration.") } }) diff --git a/Backend/main.go b/Backend/main.go index dbb6d87..e74d7bb 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -46,7 +46,8 @@ 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("/user/{action}", endpoints.UserHandler) // server.HandleFunc("/user/login", endpoints.LoginHandler) // server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) diff --git a/Backend/models/user.go b/Backend/models/user.go index e3d94a9..646a561 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -57,6 +57,34 @@ 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 { + fmt.Printf("Error preparing query statement %v\n", err) + 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 diff --git a/DB/initdb/01_schema.sql b/DB/initdb/01_schema.sql index 21e48ea..70cfefe 100755 --- a/DB/initdb/01_schema.sql +++ b/DB/initdb/01_schema.sql @@ -104,7 +104,7 @@ CREATE TABLE "s_abwesenheit_typen" ( "arbeitszeit_equivalent" float4 NOT NULL ); -COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; 1=Arbeitszeit auffüllen; 2=Arbeitszeit austauschen'; +COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; -1=Arbeitszeit auffüllen; <=1 => Arbeitszeit'; -- Adds crypto extension diff --git a/DocumentCreator/templates/abrechnung.typ b/DocumentCreator/templates/abrechnung.typ index ade04ab..5eb7bb0 100644 --- a/DocumentCreator/templates/abrechnung.typ +++ b/DocumentCreator/templates/abrechnung.typ @@ -11,7 +11,7 @@ columns: (3fr, .65fr), align: left + horizon, inset: .5em, - [#meta.EmployeeName -- #meta.TimeRange], grid.cell(rowspan: 2)[#image("static/logo.png")], + [#meta.EmployeeName -- #meta.TimeRange], grid.cell(rowspan: 2)[#image("/static/logo.png")], [Arbeitszeitrechnung maschinell erstellt am #meta.CurrentTimestamp], ) ]) -- 2.49.1 From a6ea625e8ffd13142016c67c71b9bc966a242996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Wed, 3 Dec 2025 00:38:26 +0100 Subject: [PATCH 18/35] upped test coverage of helpers --- Backend/helper/strings_test.go | 47 ++++++++++++++ Backend/helper/time_test.go | 56 ++++++++++++++++- Backend/helper/types_test.go | 26 ++++++++ Backend/helper/web_test.go | 112 +++++++++++++++++++++++++++++++++ 4 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 Backend/helper/strings_test.go create mode 100644 Backend/helper/types_test.go create mode 100644 Backend/helper/web_test.go 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_test.go b/Backend/helper/time_test.go index bb10294..4e6e67e 100644 --- a/Backend/helper/time_test.go +++ b/Backend/helper/time_test.go @@ -26,7 +26,7 @@ func TestGetMonday(t *testing.T) { } } -func TestFormatDuration(t *testing.T) { +func TestFormatDurationFill(t *testing.T) { testCases := []struct { name string duration time.Duration @@ -48,6 +48,22 @@ func TestFormatDuration(t *testing.T) { } } +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 @@ -116,3 +132,41 @@ func TestFormatGermanDayOfWeek(t *testing.T) { }) } } + +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) +// } +// } -- 2.49.1 From f73c2b1a962285092774164b002737758b02db92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 5 Dec 2025 15:03:34 +0100 Subject: [PATCH 19/35] tried to add untertags krank --- .gitea/workflows/build.yaml | 4 +- Backend/endpoints/pdf-create.go | 19 +- .../{team_presence.go => team-presence.go} | 0 Backend/endpoints/time.go | 2 +- Backend/models/absence.go | 8 +- Backend/models/absence_test.go | 92 ++++ Backend/models/booking_test.go | 12 +- Backend/models/iworkday.go | 59 +++ Backend/models/user.go | 14 +- Backend/models/user_test.go | 2 +- Backend/models/workDay.go | 165 +------ Backend/models/workDay_test.go | 184 +++++--- Backend/models/workWeek.go | 10 +- Backend/templates/pdf.templ | 129 +++--- Backend/templates/pdf_templ.go | 428 ++++-------------- Backend/templates/teamComponents.templ | 2 +- Backend/templates/teamComponents_templ.go | 2 +- Backend/templates/timePage.templ | 3 +- Backend/templates/timePage_templ.go | 19 +- DB/initdb/01_schema.sql | 2 +- DocumentCreator/templates/abrechnung.typ | 5 +- 21 files changed, 508 insertions(+), 653 deletions(-) rename Backend/endpoints/{team_presence.go => team-presence.go} (100%) create mode 100644 Backend/models/absence_test.go create mode 100644 Backend/models/iworkday.go diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index a73ba14..e112b79 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -73,5 +73,5 @@ jobs: push: true context: Backend tags: | - git.letsstein.de/tom/arbeitszeitmessung:latest - git.letsstein.de/tom/arbeitszeitmessung:${{ github.ref_name }} + git.letsstein.de/tom/arbeitszeitmessung-webserver:latest + git.letsstein.de/tom/arbeitszeitmessung-webserver:${{ github.ref_name }} diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index 7d1ae45..b70ea52 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -20,7 +20,7 @@ func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, erro var typstDays []typstDay for _, day := range days { var thisTypstDay typstDay - work, pause, overtime := day.GetTimesReal(u, models.WorktimeBaseWeek) + work, pause, overtime := day.GetTimesVirtual(u, models.WorktimeBaseWeek) thisTypstDay.Date = day.Date().Format(DE_DATE) thisTypstDay.Worktime = helper.FormatDurationFill(work, true) thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) @@ -45,6 +45,16 @@ func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDay typstDayPart.IsWorkDay = true typstDayParts = append(typstDayParts, typstDayPart) } + if day.IsKurzArbeit() { + tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user) + typstDayParts = append(typstDayParts, typstDayPart{ + BookingFrom: tsFrom.Format("15:04"), + BookingTo: tsTo.Format("15:04"), + WorkType: "Kurzarbeit", + IsWorkDay: true, + }) + } + } else { absentDay, _ := day.(*models.Absence) typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: absentDay.AbwesenheitTyp.Name}) @@ -122,7 +132,7 @@ func createEmployeReport(employee models.User, startDate, endDate time.Time) (by var actualHours time.Duration for _, day := range workingDays { - actualHours += day.TimeWorkVirtual(employee) + actualHours += day.GetWorktimeVirtual(employee, models.WorktimeBaseDay) } worktimeBalance := actualHours - targetHours @@ -162,8 +172,8 @@ func PDFHandler(w http.ResponseWriter, r *http.Request) { weeks := models.GetDays(user, startDate, endDate, false) var aggregatedOvertime, aggregatedWorkTime time.Duration for _, day := range weeks { - aggregatedOvertime += day.TimeOvertimeReal(user) - aggregatedWorkTime += day.TimeWorkVirtual(user) + aggregatedOvertime += day.GetOvertimeReal(user, models.WorktimeBaseWeek) + aggregatedWorkTime += day.GetWorktimeVirtual(user, models.WorktimeBaseWeek) } typstDays, err := convertDaysToTypst(weeks, user) @@ -193,6 +203,7 @@ 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"` 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/time.go b/Backend/endpoints/time.go index d8bb4c3..a3cf760 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -84,7 +84,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) { if day.Date().Before(lastSub) { continue } - aggregatedOvertime += day.TimeOvertimeReal(user) + aggregatedOvertime += day.GetOvertimeReal(user, models.WorktimeBaseDay) } if reportedOvertime, err := user.GetReportedOvertime(); err == nil { user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute) diff --git a/Backend/models/absence.go b/Backend/models/absence.go index 0f99bbb..493261e 100644 --- a/Backend/models/absence.go +++ b/Backend/models/absence.go @@ -50,14 +50,14 @@ func (a *Absence) IsMultiDay() bool { } func (a *Absence) GetWorktimeReal(u User, base WorktimeBase) time.Duration { - if a.AbwesenheitTyp.WorkTime <= 1 { + if a.AbwesenheitTyp.WorkTime <= 0 { return 0 } switch base { case WorktimeBaseDay: - return u.ArbeitszeitProTag() + return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100) case WorktimeBaseWeek: - return u.ArbeitszeitProWoche() / 5 + return u.ArbeitszeitProWocheFrac(0.2 * float32(a.AbwesenheitTyp.WorkTime) / 100) } return 0 } @@ -66,7 +66,7 @@ func (a *Absence) GetPausetimeReal(u User, base WorktimeBase) time.Duration { } func (a *Absence) GetOvertimeReal(u User, base WorktimeBase) time.Duration { - if a.AbwesenheitTyp.WorkTime > 1 { + if a.AbwesenheitTyp.WorkTime > 0 { return 0 } switch base { diff --git a/Backend/models/absence_test.go b/Backend/models/absence_test.go new file mode 100644 index 0000000..7e40dea --- /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.GetWorktimeReal(testUser, models.WorktimeBaseDay) + 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.GetWorktimeReal(testUser, models.WorktimeBaseWeek) + 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_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/iworkday.go b/Backend/models/iworkday.go new file mode 100644 index 0000000..cf0344e --- /dev/null +++ b/Backend/models/iworkday.go @@ -0,0 +1,59 @@ +package models + +import ( + "arbeitszeitmessung/helper" + "log" + "time" +) + +type IWorkDay interface { + Date() time.Time + ToString() string + IsWorkDay() bool + IsKurzArbeit() bool + GetDayProgress(User) int8 + RequiresAction() bool + GetWorktimeReal(User, WorktimeBase) time.Duration + GetPausetimeReal(User, WorktimeBase) time.Duration + GetOvertimeReal(User, WorktimeBase) time.Duration + GetWorktimeVirtual(User, WorktimeBase) time.Duration + GetPausetimeVirtual(User, WorktimeBase) time.Duration + GetOvertimeVirtual(User, WorktimeBase) time.Duration + GetTimesReal(User, WorktimeBase) (work, pause, overtime time.Duration) + GetTimesVirtual(User, WorktimeBase) (work, pause, overtime time.Duration) +} + +func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay { + var allDays map[string]IWorkDay = make(map[string]IWorkDay) + + for _, day := range GetWorkDays(user, tsFrom, tsTo) { + allDays[day.Date().Format(time.DateOnly)] = &day + } + absences, err := GetAbsencesByCardUID(user.CardUID, tsFrom, tsTo) + if err != nil { + log.Println("Error gettings absences for all Days!", err) + return nil + } + for _, day := range absences { + if helper.IsWeekend(day.Date()) { + continue + } + // Absence should be integrated in workday + switch { + case day.AbwesenheitTyp.WorkTime < 0: + if workDay, ok := allDays[day.Date().Format(time.DateOnly)].(*WorkDay); ok { + workDay.kurzArbeit = true + workDay.kurzArbeitAbsence = day + } + case day.AbwesenheitTyp.WorkTime < 100: + if workDay, ok := allDays[day.Date().Format(time.DateOnly)].(*WorkDay); ok { + workDay.worktimeAbsece = day + } + default: + allDays[day.Date().Format(time.DateOnly)] = &day + } + } + + sortedDays := sortDays(allDays, orderedForward) + return sortedDays +} diff --git a/Backend/models/user.go b/Backend/models/user.go index 646a561..8f654e0 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -113,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 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 b7d3254..adf8895 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -10,39 +10,17 @@ import ( "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 - GetWorktimeReal(User, WorktimeBase) time.Duration - GetPausetimeReal(User, WorktimeBase) time.Duration - GetOvertimeReal(User, WorktimeBase) time.Duration - GetWorktimeVirtual(User, WorktimeBase) time.Duration - GetPausetimeVirtual(User, WorktimeBase) time.Duration - GetOvertimeVirtual(User, WorktimeBase) time.Duration - GetTimesReal(User, WorktimeBase) (work, pause, overtime time.Duration) - GetTimesVirtual(User, WorktimeBase) (work, pause, overtime time.Duration) -} - 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 } type WorktimeBase string @@ -52,62 +30,39 @@ const ( WorktimeBaseDay WorktimeBase = "day" ) -func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay { - var allDays map[string]IWorkDay = make(map[string]IWorkDay) - - for _, day := range GetWorkDays(user, tsFrom, tsTo) { - allDays[day.Date().Format(time.DateOnly)] = &day - } - absences, err := GetAbsencesByCardUID(user.CardUID, tsFrom, tsTo) - if err != nil { - log.Println("Error gettings absences for all Days!", err) - return nil - } - for _, day := range absences { - if helper.IsWeekend(day.Date()) { - continue - } - if day.AbwesenheitTyp.WorkTime == 1 { - if workDay, ok := allDays[day.Date().Format(time.DateOnly)].(*WorkDay); ok && len(workDay.Bookings) > 0 { - workDay.kurzArbeit = true - workDay.kurzArbeitAbsence = day - } - } else { - allDays[day.Date().Format(time.DateOnly)] = &day - } - } - - sortedDays := sortDays(allDays, orderedForward) - return sortedDays -} - // Gets the time as is in the db (with corrected pause times) func (d *WorkDay) GetWorktimeReal(u User, base WorktimeBase) time.Duration { work, pause := calcWorkPause(d.Bookings) work, pause = correctWorkPause(work, pause) - return work + if (d.worktimeAbsece != Absence{}) { + work += d.worktimeAbsece.GetWorktimeReal(u, WorktimeBaseDay) + } + return work.Round(time.Minute) } // Gets the corrected pause times based on db entries func (d *WorkDay) GetPausetimeReal(u User, base WorktimeBase) time.Duration { work, pause := calcWorkPause(d.Bookings) work, pause = correctWorkPause(work, pause) - return pause + return pause.Round(time.Minute) } // Returns the overtime based on the db entries func (d *WorkDay) GetOvertimeReal(u User, base WorktimeBase) time.Duration { work, pause := calcWorkPause(d.Bookings) work, pause = correctWorkPause(work, pause) + if (d.worktimeAbsece != Absence{}) { + work += d.worktimeAbsece.GetWorktimeReal(u, base) + } var targetHours time.Duration switch base { case WorktimeBaseDay: targetHours = u.ArbeitszeitProTag() case WorktimeBaseWeek: - targetHours = u.ArbeitszeitProWoche() / 5 + targetHours = u.ArbeitszeitProWocheFrac(0.2) } - return work - targetHours + return (work - targetHours).Round(time.Minute) } // Returns the worktime based on absence or kurzarbeit @@ -119,9 +74,10 @@ func (d *WorkDay) GetWorktimeVirtual(u User, base WorktimeBase) time.Duration { case WorktimeBaseDay: return u.ArbeitszeitProTag() case WorktimeBaseWeek: - return u.ArbeitszeitProWoche() / 5 + return u.ArbeitszeitProWocheFrac(0.2) + default: + return 0 } - return 0 } func (d *WorkDay) GetPausetimeVirtual(u User, base WorktimeBase) time.Duration { @@ -136,9 +92,9 @@ func (d *WorkDay) GetOvertimeVirtual(u User, base WorktimeBase) time.Duration { case WorktimeBaseDay: targetHours = u.ArbeitszeitProTag() case WorktimeBaseWeek: - targetHours = u.ArbeitszeitProWoche() / 5 + targetHours = u.ArbeitszeitProWocheFrac(0.2) } - return work - targetHours + return (work - targetHours).Round(time.Minute) } func (d *WorkDay) GetTimesReal(u User, base WorktimeBase) (work, pause, overtime time.Duration) { @@ -217,70 +173,10 @@ func (d *WorkDay) GenerateKurzArbeitBookings(u User) (time.Time, time.Time) { return timeFrom, timeTo } -func (d *WorkDay) TimeWorkVirtual(u User) time.Duration { - if d.IsKurzArbeit() { - return u.ArbeitszeitProTag() - } - return d.workTime -} - func (d *WorkDay) GetKurzArbeit() *Absence { return &d.kurzArbeitAbsence } -func (d *WorkDay) TimeWorkReal(u User) time.Duration { - d.realWorkTime, d.realPauseTime = 0, 0 - var lastBooking Booking - for _, booking := range d.Bookings { - if booking.CheckInOut%2 == 1 { - if !lastBooking.Timestamp.IsZero() { - d.realPauseTime += booking.Timestamp.Sub(lastBooking.Timestamp) - } - } else { - d.realWorkTime += booking.Timestamp.Sub(lastBooking.Timestamp) - } - lastBooking = booking - } - if helper.IsSameDate(d.Date(), time.Now()) && len(d.Bookings)%2 == 1 { - d.realWorkTime += time.Since(lastBooking.Timestamp.Local()) - } - // slog.Debug("Calculated RealWorkTime for user", "user", u, slog.String("worktime", d.realWorkTime.String())) - return d.realWorkTime -} - -func (d *WorkDay) TimeOvertimeReal(u User) time.Duration { - workTime := d.TimeWorkVirtual(u) - if workTime == 0 { - workTime, _ = d.TimePauseReal(u) - } - if helper.IsWeekend(d.Day) && len(d.Bookings) == 0 { - return 0 - } - var overtime time.Duration - overtime = workTime - u.ArbeitszeitProTag() - return overtime -} - -func (d *WorkDay) TimePauseReal(u User) (work, pause time.Duration) { - if d.realWorkTime == 0 { - d.TimeWorkReal(u) - } - d.workTime, d.pauseTime = d.realWorkTime, d.realPauseTime - if d.realWorkTime <= 6*time.Hour || d.realPauseTime > 45*time.Minute { - return d.realWorkTime, d.realPauseTime - } - if d.realWorkTime <= (9*time.Hour) && d.realPauseTime < 30*time.Minute { - diff := 30*time.Minute - d.pauseTime - d.workTime -= diff - d.pauseTime += diff - } else if d.realPauseTime < 45*time.Minute { - diff := 45*time.Minute - d.pauseTime - d.workTime -= diff - d.pauseTime += diff - } - return d.workTime, d.pauseTime -} - func (d *WorkDay) ToString() string { return fmt.Sprintf("WorkDay: %s with %d bookings and worktime: %s", d.Date().Format(time.DateOnly), len(d.Bookings), helper.FormatDuration(d.workTime)) } @@ -388,7 +284,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) } @@ -400,18 +295,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 { @@ -424,19 +307,7 @@ func (d *WorkDay) GetDayProgress(u User) int8 { if d.RequiresAction() { return -1 } - workTime := d.TimeWorkVirtual(u) + workTime := d.GetWorktimeVirtual(u, WorktimeBaseDay) 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 5b1aef7..cae5d38 100644 --- a/Backend/models/workDay_test.go +++ b/Backend/models/workDay_test.go @@ -22,59 +22,141 @@ var testWorkDay = models.WorkDay{ 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.GetWorktimeReal(testUser, models.WorktimeBaseDay) + 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.GetWorktimeReal(testUser, models.WorktimeBaseWeek) + 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.GetPausetimeReal(testUser, models.WorktimeBaseDay) + 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.GetPausetimeReal(testUser, models.WorktimeBaseWeek) + 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 beaf504..ea353d0 100644 --- a/Backend/models/workWeek.go +++ b/Backend/models/workWeek.go @@ -47,18 +47,20 @@ func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek { } func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Duration) { + slog.Debug("Populating Workweek for user", "user", w.User) slog.Debug("Got Days with overtime and worktime", slog.String("worktime", worktime.String()), slog.String("overtime", overtime.String())) w.Days = GetDays(w.User, w.WeekStart, w.WeekStart.Add(6*24*time.Hour), false) for _, day := range w.Days { - work, _ := day.TimePauseReal(w.User) - w.Worktime += work - w.WorkTimeVirtual += day.TimeWorkVirtual(w.User) + w.Worktime += day.GetWorktimeReal(w.User, WorktimeBaseDay) + w.WorkTimeVirtual += day.GetWorktimeVirtual(w.User, WorktimeBaseDay) } - slog.Debug("Got worktime for user", "user", w.User, "worktime", w.Worktime.String(), "virtualWorkTime", w.WorkTimeVirtual.String()) + 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) diff --git a/Backend/templates/pdf.templ b/Backend/templates/pdf.templ index c00d786..332ab33 100644 --- a/Backend/templates/pdf.templ +++ b/Backend/templates/pdf.templ @@ -58,71 +58,70 @@ templ CheckboxComponent(id, label string) {
} -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.GetAllWorkTimesVirtual(e) }} - @ColorDuration(work, noBorder) - @ColorDuration(pause, noBorder) - @ColorDuration(overtime, noBorder+" border-r-0") - if day.Date().Weekday() == time.Friday { -

Wochenende

- } - } -
-
-} - +// 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 := "" diff --git a/Backend/templates/pdf_templ.go b/Backend/templates/pdf_templ.go index a15eca1..997a431 100644 --- a/Backend/templates/pdf_templ.go +++ b/Backend/templates/pdf_templ.go @@ -177,7 +177,71 @@ func CheckboxComponent(id, label string) templ.Component { }) } -func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays []models.IWorkDay, tsStart time.Time, tsEnd time.Time) templ.Component { +// 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 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -199,378 +263,42 @@ func PDFReportEmploye(e models.User, overtime, worktime time.Duration, workDays } ctx = templ.ClearChildren(ctx) - _, kw := tsStart.ISOWeek() - noBorder := "" - templ_7745c5c3_Err = Base().Render(ctx, templ_7745c5c3_Buffer) + color := "" + if d.Abs() < time.Minute { + color = "text-neutral-300" + } + var templ_7745c5c3_Var10 = []any{color + " " + classes} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vorname) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 69, Col: 45} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

Zeitraum: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(tsStart.Format("02.01.2006")) + templ_7745c5c3_Var12, 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: 70, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 132, Col: 72} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " - ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(tsEnd.Format("02.01.2006")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 70, Col: 98} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

Arbeitszeit: ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(worktime)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 71, Col: 58} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

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

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

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_Var17 = []any{noBorder} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

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

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

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

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

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

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if workDay.IsKurzArbeit() { - - timeFrom, timeTo := workDay.GenerateKurzArbeitBookings(e) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(timeFrom.Format("15:04")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 103, Col: 36} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(timeTo.Format("15:04")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 104, Col: 34} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "

Kurzarbeit

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

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

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - work, pause, overtime := day.GetAllWorkTimesVirtual(e) - templ_7745c5c3_Err = ColorDuration(work, noBorder).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = ColorDuration(pause, noBorder).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = ColorDuration(overtime, noBorder+" border-r-0").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if day.Date().Weekday() == time.Friday { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

Wochenende

") - 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 - } - return nil - }) -} - -func ColorDuration(d time.Duration, classes string) templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var28 := templ.GetChildren(ctx) - if templ_7745c5c3_Var28 == nil { - templ_7745c5c3_Var28 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - - color := "" - if d.Abs() < time.Minute { - color = "text-neutral-300" - } - var templ_7745c5c3_Var29 = []any{color + " " + classes} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDurationFill(d, true)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 133, Col: 72} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/teamComponents.templ b/Backend/templates/teamComponents.templ index 086bd8d..b0e324a 100644 --- a/Backend/templates/teamComponents.templ +++ b/Backend/templates/teamComponents.templ @@ -36,7 +36,7 @@ templ defaultWeekDayComponent(u models.User, day models.IWorkDay) { if day.IsWorkDay() { {{ workDay, _ := day.(*models.WorkDay) - work, pause, _ := workDay.GetAllWorkTimesReal(u) + work, pause, _ := workDay.GetTimesReal(u, models.WorktimeBaseDay) }} if !workDay.RequiresAction() {
diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index 6314bce..80a940e 100644 --- a/Backend/templates/teamComponents_templ.go +++ b/Backend/templates/teamComponents_templ.go @@ -179,7 +179,7 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component if day.IsWorkDay() { workDay, _ := day.(*models.WorkDay) - work, pause, _ := workDay.GetAllWorkTimesReal(u) + work, pause, _ := workDay.GetTimesReal(u, models.WorktimeBaseDay) if !workDay.RequiresAction() { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") if templ_7745c5c3_Err != nil { diff --git a/Backend/templates/timePage.templ b/Backend/templates/timePage.templ index 88cd3ec..a59bc47 100644 --- a/Backend/templates/timePage.templ +++ b/Backend/templates/timePage.templ @@ -103,7 +103,8 @@ templ defaultDayComponent(day models.IWorkDay) { if day.IsWorkDay() { {{ workDay, _ := day.(*models.WorkDay) - work, pause, overtime := workDay.GetAllWorkTimesReal(user) + work, pause, overtime := workDay.GetTimesVirtual(user, models.WorktimeBaseDay) + work = workDay.GetWorktimeReal(user, models.WorktimeBaseDay) }} if day.RequiresAction() {

Bitte anpassen

diff --git a/Backend/templates/timePage_templ.go b/Backend/templates/timePage_templ.go index 209fa28..264a21b 100644 --- a/Backend/templates/timePage_templ.go +++ b/Backend/templates/timePage_templ.go @@ -297,7 +297,8 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { if day.IsWorkDay() { workDay, _ := day.(*models.WorkDay) - work, pause, overtime := workDay.GetAllWorkTimesReal(user) + work, pause, overtime := workDay.GetTimesVirtual(user, models.WorktimeBaseDay) + work = workDay.GetWorktimeReal(user, models.WorktimeBaseDay) if day.RequiresAction() { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

Bitte anpassen

") if templ_7745c5c3_Err != nil { @@ -312,7 +313,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(work)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 113, Col: 155} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 114, Col: 155} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -335,7 +336,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(pause)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 116, Col: 173} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 117, Col: 173} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -358,7 +359,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(overtime)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 121, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 122, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -391,7 +392,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("time-" + day.Date().Format(time.DateOnly)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 130, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 131, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { @@ -507,7 +508,7 @@ func absentInput(a models.Absence) templ.Component { var templ_7745c5c3_Var21 string templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateFrom.Format(time.DateOnly)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 162, Col: 79} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 163, Col: 79} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { @@ -520,7 +521,7 @@ func absentInput(a models.Absence) templ.Component { var templ_7745c5c3_Var22 string templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateTo.Format(time.DateOnly)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 163, Col: 75} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 164, Col: 75} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { @@ -533,7 +534,7 @@ func absentInput(a models.Absence) templ.Component { var templ_7745c5c3_Var23 string templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(a.AbwesenheitTyp.Id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 164, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 165, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { @@ -546,7 +547,7 @@ func absentInput(a models.Absence) templ.Component { var templ_7745c5c3_Var24 string templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(a.CounterId) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 165, Col: 54} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 166, Col: 54} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { diff --git a/DB/initdb/01_schema.sql b/DB/initdb/01_schema.sql index 70cfefe..b434f4c 100755 --- a/DB/initdb/01_schema.sql +++ b/DB/initdb/01_schema.sql @@ -104,7 +104,7 @@ CREATE TABLE "s_abwesenheit_typen" ( "arbeitszeit_equivalent" float4 NOT NULL ); -COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; -1=Arbeitszeit auffüllen; <=1 => Arbeitszeit'; +COMMENT ON COLUMN "s_abwesenheit_typen"."arbeitszeit_equivalent" IS '0=keine Arbeitszeit; -1=Arbeitszeit auffüllen; <=1 - 100 => Arbeitszeit pro Tag prozentual'; -- Adds crypto extension diff --git a/DocumentCreator/templates/abrechnung.typ b/DocumentCreator/templates/abrechnung.typ index 5eb7bb0..ee71ebc 100644 --- a/DocumentCreator/templates/abrechnung.typ +++ b/DocumentCreator/templates/abrechnung.typ @@ -37,7 +37,7 @@ [Zeitraum: #meta.TimeRange] table( - columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1.25fr), + columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, .875fr, 1.25fr), fill: (x, y) => if y == 0 { oklch(87%, 0, 0deg) }, table-header( @@ -68,11 +68,12 @@ ] }, [#day.Worktime], + [#day.Kurzarbeit], [#day.Pausetime], [#day.Overtime], ) if day.IsFriday { - ( table.cell(colspan: 7, fill: oklch(87%, 0, 0deg))[Wochenende], ) // note the trailing comma + ( table.cell(colspan: 8, fill: oklch(87%, 0, 0deg))[Wochenende], ) // note the trailing comma } } ) -- 2.49.1 From 1ccc19b85cb1c2626f287688878457aec79eb2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 12 Dec 2025 06:31:03 +0100 Subject: [PATCH 20/35] removed and refactored virtual and real worktime --- Backend/endpoints/auto-kurzarbeit.go | 4 +- Backend/endpoints/pdf-create.go | 8 +-- Backend/endpoints/time.go | 2 +- Backend/models/absence.go | 41 +++++++++++++++- Backend/models/iworkday.go | 12 ++--- Backend/models/workDay.go | 60 +++++------------------ Backend/models/workDay_test.go | 8 +-- Backend/models/workWeek.go | 12 ++--- Backend/templates/teamComponents.templ | 4 +- Backend/templates/teamComponents_templ.go | 4 +- Backend/templates/timePage.templ | 4 +- Backend/templates/timePage_templ.go | 4 +- 12 files changed, 79 insertions(+), 84 deletions(-) diff --git a/Backend/endpoints/auto-kurzarbeit.go b/Backend/endpoints/auto-kurzarbeit.go index 3a1e394..10b2cd5 100644 --- a/Backend/endpoints/auto-kurzarbeit.go +++ b/Backend/endpoints/auto-kurzarbeit.go @@ -46,11 +46,11 @@ func fillKurzarbeit(r *http.Request, w http.ResponseWriter) { if !day.IsKurzArbeit() || !day.IsWorkDay() { continue } - if day.GetWorktimeReal(user, models.WorktimeBaseDay) >= day.GetWorktimeVirtual(user, models.WorktimeBaseDay) { + if day.GetWorktime(user, models.WorktimeBaseDay, false) >= day.GetWorktime(user, models.WorktimeBaseDay, true) { continue } - worktimeKurzarbeit := day.GetWorktimeVirtual(user, models.WorktimeBaseDay) - day.GetWorktimeReal(user, models.WorktimeBaseDay) + 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 diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index b70ea52..9f20759 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -20,7 +20,7 @@ func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, erro var typstDays []typstDay for _, day := range days { var thisTypstDay typstDay - work, pause, overtime := day.GetTimesVirtual(u, models.WorktimeBaseWeek) + work, pause, overtime := day.GetTimes(u, models.WorktimeBaseWeek, true) thisTypstDay.Date = day.Date().Format(DE_DATE) thisTypstDay.Worktime = helper.FormatDurationFill(work, true) thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true) @@ -132,7 +132,7 @@ func createEmployeReport(employee models.User, startDate, endDate time.Time) (by var actualHours time.Duration for _, day := range workingDays { - actualHours += day.GetWorktimeVirtual(employee, models.WorktimeBaseDay) + actualHours += day.GetWorktime(employee, models.WorktimeBaseDay, true) } worktimeBalance := actualHours - targetHours @@ -172,8 +172,8 @@ func PDFHandler(w http.ResponseWriter, r *http.Request) { weeks := models.GetDays(user, startDate, endDate, false) var aggregatedOvertime, aggregatedWorkTime time.Duration for _, day := range weeks { - aggregatedOvertime += day.GetOvertimeReal(user, models.WorktimeBaseWeek) - aggregatedWorkTime += day.GetWorktimeVirtual(user, models.WorktimeBaseWeek) + aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseWeek, false) + aggregatedWorkTime += day.GetWorktime(user, models.WorktimeBaseWeek, true) } typstDays, err := convertDaysToTypst(weeks, user) diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index a3cf760..15a6375 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -84,7 +84,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) { if day.Date().Before(lastSub) { continue } - aggregatedOvertime += day.GetOvertimeReal(user, models.WorktimeBaseDay) + aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, false) } if reportedOvertime, err := user.GetReportedOvertime(); err == nil { user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute) diff --git a/Backend/models/absence.go b/Backend/models/absence.go index 493261e..1ff28e2 100644 --- a/Backend/models/absence.go +++ b/Backend/models/absence.go @@ -49,6 +49,43 @@ func (a *Absence) IsMultiDay() bool { return !a.DateFrom.Equal(a.DateTo) } +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) + } + return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100) + case WorktimeBaseWeek: + if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit { + return u.ArbeitszeitProTagFrac(0.2) + } + return u.ArbeitszeitProWocheFrac(0.2 * float32(a.AbwesenheitTyp.WorkTime) / 100) + } + return 0 +} + +func (a *Absence) GetPausetime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + return 0 +} + +func (a *Absence) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + if a.AbwesenheitTyp.WorkTime > 0 { + return 0 + } + switch base { + case WorktimeBaseDay: + return -u.ArbeitszeitProTagFrac(1) + case WorktimeBaseWeek: + return -u.ArbeitszeitProWocheFrac(0.2) + } + return 0 +} + +func (a *Absence) GetTimes(u User, base WorktimeBase, includeKurzarbeit bool) (work, pause, overtime time.Duration) { + return a.GetWorktime(u, base, includeKurzarbeit), a.GetPausetime(u, base, includeKurzarbeit), a.GetOvertime(u, base, includeKurzarbeit) +} + func (a *Absence) GetWorktimeReal(u User, base WorktimeBase) time.Duration { if a.AbwesenheitTyp.WorkTime <= 0 { return 0 @@ -71,9 +108,9 @@ func (a *Absence) GetOvertimeReal(u User, base WorktimeBase) time.Duration { } switch base { case WorktimeBaseDay: - return -u.ArbeitszeitProTag() + return -u.ArbeitszeitProTagFrac(1) case WorktimeBaseWeek: - return -u.ArbeitszeitProWoche() / 5 + return -u.ArbeitszeitProWocheFrac(0.2) } return 0 } diff --git a/Backend/models/iworkday.go b/Backend/models/iworkday.go index cf0344e..4970ba6 100644 --- a/Backend/models/iworkday.go +++ b/Backend/models/iworkday.go @@ -13,14 +13,10 @@ type IWorkDay interface { IsKurzArbeit() bool GetDayProgress(User) int8 RequiresAction() bool - GetWorktimeReal(User, WorktimeBase) time.Duration - GetPausetimeReal(User, WorktimeBase) time.Duration - GetOvertimeReal(User, WorktimeBase) time.Duration - GetWorktimeVirtual(User, WorktimeBase) time.Duration - GetPausetimeVirtual(User, WorktimeBase) time.Duration - GetOvertimeVirtual(User, WorktimeBase) time.Duration - GetTimesReal(User, WorktimeBase) (work, pause, overtime time.Duration) - GetTimesVirtual(User, WorktimeBase) (work, pause, overtime time.Duration) + GetWorktime(User, WorktimeBase, bool) time.Duration + GetPausetime(User, WorktimeBase, bool) time.Duration + GetTimes(User, WorktimeBase, bool) (work, pause, overtime time.Duration) + GetOvertime(User, WorktimeBase, bool) time.Duration } func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay { diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index adf8895..e82bd5a 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -31,30 +31,28 @@ const ( ) // Gets the time as is in the db (with corrected pause times) -func (d *WorkDay) GetWorktimeReal(u User, base WorktimeBase) time.Duration { +func (d *WorkDay) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration { + if includeKurzarbeit && d.IsKurzArbeit() { + return d.kurzArbeitAbsence.GetWorktime(u, base, true) + } work, pause := calcWorkPause(d.Bookings) work, pause = correctWorkPause(work, pause) if (d.worktimeAbsece != Absence{}) { - work += d.worktimeAbsece.GetWorktimeReal(u, WorktimeBaseDay) + work += d.worktimeAbsece.GetWorktimeReal(u, base) } return work.Round(time.Minute) } // Gets the corrected pause times based on db entries -func (d *WorkDay) GetPausetimeReal(u User, base WorktimeBase) time.Duration { +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) GetOvertimeReal(u User, base WorktimeBase) time.Duration { - work, pause := calcWorkPause(d.Bookings) - work, pause = correctWorkPause(work, pause) - if (d.worktimeAbsece != Absence{}) { - work += d.worktimeAbsece.GetWorktimeReal(u, base) - } - +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: @@ -65,44 +63,8 @@ func (d *WorkDay) GetOvertimeReal(u User, base WorktimeBase) time.Duration { return (work - targetHours).Round(time.Minute) } -// Returns the worktime based on absence or kurzarbeit -func (d *WorkDay) GetWorktimeVirtual(u User, base WorktimeBase) time.Duration { - if !d.IsKurzArbeit() { - return d.GetWorktimeReal(u, base) - } - switch base { - case WorktimeBaseDay: - return u.ArbeitszeitProTag() - case WorktimeBaseWeek: - return u.ArbeitszeitProWocheFrac(0.2) - default: - return 0 - } -} - -func (d *WorkDay) GetPausetimeVirtual(u User, base WorktimeBase) time.Duration { - return d.GetPausetimeReal(u, base) -} - -func (d *WorkDay) GetOvertimeVirtual(u User, base WorktimeBase) time.Duration { - work := d.GetWorktimeVirtual(u, base) - - var targetHours time.Duration - switch base { - case WorktimeBaseDay: - targetHours = u.ArbeitszeitProTag() - case WorktimeBaseWeek: - targetHours = u.ArbeitszeitProWocheFrac(0.2) - } - return (work - targetHours).Round(time.Minute) -} - -func (d *WorkDay) GetTimesReal(u User, base WorktimeBase) (work, pause, overtime time.Duration) { - return d.GetWorktimeReal(u, base), d.GetPausetimeReal(u, base), d.GetOvertimeReal(u, base) -} - -func (d *WorkDay) GetTimesVirtual(u User, base WorktimeBase) (work, pause, overtime time.Duration) { - return d.GetWorktimeVirtual(u, base), d.GetPausetimeVirtual(u, base), d.GetOvertimeVirtual(u, base) +func (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) { @@ -307,7 +269,7 @@ func (d *WorkDay) GetDayProgress(u User) int8 { if d.RequiresAction() { return -1 } - workTime := d.GetWorktimeVirtual(u, WorktimeBaseDay) + workTime := d.GetWorktime(u, WorktimeBaseDay, true) progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100 return int8(progress) } diff --git a/Backend/models/workDay_test.go b/Backend/models/workDay_test.go index cae5d38..9ad0e6d 100644 --- a/Backend/models/workDay_test.go +++ b/Backend/models/workDay_test.go @@ -49,7 +49,7 @@ func TestWorkdayWorktimeDay(t *testing.T) { t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) { var testCase = testWorkDay testCase.Bookings = tc.bookings - workTime := testCase.GetWorktimeReal(testUser, models.WorktimeBaseDay) + 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)) } @@ -84,7 +84,7 @@ func TestWorkdayWorktimeWeek(t *testing.T) { t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) { var testCase = testWorkDay testCase.Bookings = tc.bookings - workTime := testCase.GetWorktimeReal(testUser, models.WorktimeBaseWeek) + 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)) } @@ -119,7 +119,7 @@ func TestWorkdayPausetimeDay(t *testing.T) { t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) { var testCase = testWorkDay testCase.Bookings = tc.bookings - workTime := testCase.GetPausetimeReal(testUser, models.WorktimeBaseDay) + 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)) } @@ -154,7 +154,7 @@ func TestWorkdayPausetimeWeek(t *testing.T) { t.Run("Calc Absence Worktime: "+tc.testName, func(t *testing.T) { var testCase = testWorkDay testCase.Bookings = tc.bookings - workTime := testCase.GetPausetimeReal(testUser, models.WorktimeBaseWeek) + 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 ea353d0..168365a 100644 --- a/Backend/models/workWeek.go +++ b/Backend/models/workWeek.go @@ -20,7 +20,7 @@ type WorkWeek struct { User User WeekStart time.Time Worktime time.Duration - WorkTimeVirtual time.Duration + WorktimeVirtual time.Duration Overtime time.Duration Status WeekStatus } @@ -52,14 +52,14 @@ func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Durati w.Days = GetDays(w.User, w.WeekStart, w.WeekStart.Add(6*24*time.Hour), false) for _, day := range w.Days { - w.Worktime += day.GetWorktimeReal(w.User, WorktimeBaseDay) - w.WorkTimeVirtual += day.GetWorktimeVirtual(w.User, WorktimeBaseDay) + w.Worktime += day.GetWorktime(w.User, WorktimeBaseDay, false) + w.WorktimeVirtual += day.GetWorktime(w.User, WorktimeBaseDay, true) } - slog.Debug("Got worktime for user", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorkTimeVirtual.String()) + slog.Debug("Got worktime for user", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorktimeVirtual.String()) - w.Overtime = w.WorkTimeVirtual - w.User.ArbeitszeitProWoche() + w.Overtime = w.WorktimeVirtual - w.User.ArbeitszeitProWoche() - slog.Debug("Calculated overtime", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorkTimeVirtual.String()) + 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) diff --git a/Backend/templates/teamComponents.templ b/Backend/templates/teamComponents.templ index b0e324a..26501a4 100644 --- a/Backend/templates/teamComponents.templ +++ b/Backend/templates/teamComponents.templ @@ -36,7 +36,7 @@ templ defaultWeekDayComponent(u models.User, day models.IWorkDay) { if day.IsWorkDay() { {{ workDay, _ := day.(*models.WorkDay) - work, pause, _ := workDay.GetTimesReal(u, models.WorktimeBaseDay) + work, pause, _ := workDay.GetTimes(u, models.WorktimeBaseDay, false) }} if !workDay.RequiresAction() {
@@ -81,7 +81,7 @@ templ weekDayComponent(user models.User, day models.WorkDay) { templ workWeekComponent(week models.WorkWeek, onlyAccept bool) { {{ year, kw := week.WeekStart.ISOWeek() - progress := (float32(week.WorkTimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100 + progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100 }}
diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index 80a940e..8cdc91c 100644 --- a/Backend/templates/teamComponents_templ.go +++ b/Backend/templates/teamComponents_templ.go @@ -179,7 +179,7 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component if day.IsWorkDay() { workDay, _ := day.(*models.WorkDay) - work, pause, _ := workDay.GetTimesReal(u, models.WorktimeBaseDay) + work, pause, _ := workDay.GetTimes(u, models.WorktimeBaseDay, false) if !workDay.RequiresAction() { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") if templ_7745c5c3_Err != nil { @@ -346,7 +346,7 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { ctx = templ.ClearChildren(ctx) year, kw := week.WeekStart.ISOWeek() - progress := (float32(week.WorkTimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100 + progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100 templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err diff --git a/Backend/templates/timePage.templ b/Backend/templates/timePage.templ index a59bc47..297d2d9 100644 --- a/Backend/templates/timePage.templ +++ b/Backend/templates/timePage.templ @@ -103,8 +103,8 @@ templ defaultDayComponent(day models.IWorkDay) { if day.IsWorkDay() { {{ workDay, _ := day.(*models.WorkDay) - work, pause, overtime := workDay.GetTimesVirtual(user, models.WorktimeBaseDay) - work = workDay.GetWorktimeReal(user, models.WorktimeBaseDay) + work, pause, overtime := workDay.GetTimes(user, models.WorktimeBaseDay, true) + work = workDay.GetWorktime(user, models.WorktimeBaseDay, false) }} if day.RequiresAction() {

Bitte anpassen

diff --git a/Backend/templates/timePage_templ.go b/Backend/templates/timePage_templ.go index 264a21b..4bfba6c 100644 --- a/Backend/templates/timePage_templ.go +++ b/Backend/templates/timePage_templ.go @@ -297,8 +297,8 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { if day.IsWorkDay() { workDay, _ := day.(*models.WorkDay) - work, pause, overtime := workDay.GetTimesVirtual(user, models.WorktimeBaseDay) - work = workDay.GetWorktimeReal(user, models.WorktimeBaseDay) + work, pause, overtime := workDay.GetTimes(user, models.WorktimeBaseDay, true) + work = workDay.GetWorktime(user, models.WorktimeBaseDay, false) if day.RequiresAction() { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

Bitte anpassen

") if templ_7745c5c3_Err != nil { -- 2.49.1 From 76b23133d0d5bb94be880a4c940c1b3fb4bfc613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 12 Dec 2025 12:26:40 +0100 Subject: [PATCH 21/35] fixed #61, #62 refactored getTime variants --- Backend/endpoints/pdf-create.go | 223 +++++++++++------------ Backend/helper/paramParser/main.go | 25 +++ Backend/helper/time.go | 7 + Backend/models/absence.go | 71 -------- Backend/models/absence_test.go | 4 +- Backend/models/user.go | 33 ++++ Backend/models/workDay.go | 12 +- Backend/templates/timePage.templ | 2 +- Backend/templates/timePage_templ.go | 2 +- DocumentCreator/templates/abrechnung.typ | 24 ++- 10 files changed, 196 insertions(+), 207 deletions(-) diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index 9f20759..5c74326 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -20,13 +20,21 @@ func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, erro var typstDays []typstDay for _, day := range days { var thisTypstDay typstDay - work, pause, overtime := day.GetTimes(u, models.WorktimeBaseWeek, true) + 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(work, true) + 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) } @@ -45,7 +53,7 @@ func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDay typstDayPart.IsWorkDay = true typstDayParts = append(typstDayParts, typstDayPart) } - if day.IsKurzArbeit() { + if day.IsKurzArbeit() && len(workDay.Bookings) > 0 { tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user) typstDayParts = append(typstDayParts, typstDayPart{ BookingFrom: tsFrom.Format("15:04"), @@ -54,6 +62,9 @@ func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDay IsWorkDay: true, }) } + if workdayAbsence := workDay.GetWorktimeAbsence(); (workdayAbsence != models.Absence{}) { + typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: workdayAbsence.AbwesenheitTyp.Name}) + } } else { absentDay, _ := day.(*models.Absence) @@ -62,6 +73,90 @@ func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDay return typstDayParts } +func PDFCreateController(w http.ResponseWriter, r *http.Request) { + helper.RequiresLogin(Session, w, r) + switch r.Method { + case http.MethodGet: + user, err := models.GetUserFromSession(Session, r.Context()) + if err != nil { + log.Println("Error getting user!") + return + } + pp := paramParser.New(r.URL.Query()) + startDate := pp.ParseTimestampFallback("start_date", time.DateOnly, time.Now()) + personalNumbers := pp.ParseIntListFallback("employe_list", ",", make([]int, 0)) + + employes, err := models.GetUserByPersonalNrMulti(personalNumbers) + if err != nil { + slog.Warn("Error getting employes!", slog.Any("Error", err)) + return + } + + output, err := createReports(user, employes, startDate) + if err != nil { + slog.Warn("Could not create pdf report", slog.Any("Error", err)) + } + + w.Header().Set("Content-type", "application/pdf") + output.WriteTo(w) + w.WriteHeader(http.StatusOK) + + default: + http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) + } +} + +func createReports(user models.User, employes []models.User, startDate time.Time) (bytes.Buffer, error) { + startDate = helper.GetFirstOfMonth(startDate) + endDate := startDate.AddDate(0, 1, -1) + + var employeData []typstData + for _, employe := range employes { + if data, err := createEmployeReport(employe, startDate, endDate); err != nil { + slog.Warn("Error when creating employeReport", slog.Any("user", employe), slog.Any("error", err)) + } else { + employeData = append(employeData, data) + } + } + return renderPDF(employeData) +} + +func createEmployeReport(employee models.User, startDate, endDate time.Time) (typstData, error) { + targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)) + workDaysThisMonth := models.GetDays(employee, startDate, endDate.AddDate(0, 0, 1), false) + + slog.Debug("Baseline Working hours", "targetHours", targetHoursThisMonth.Hours()) + + var workHours, kurzarbeitHours time.Duration + for _, day := range workDaysThisMonth { + tmpvirtualHours := day.GetWorktime(employee, models.WorktimeBaseDay, true) + tmpactualHours := day.GetWorktime(employee, models.WorktimeBaseDay, false) + if day.IsKurzArbeit() && tmpvirtualHours > tmpactualHours { + slog.Debug("Adding kurzarbeit to workday", "day", day.Date()) + kurzarbeitHours += tmpvirtualHours - tmpactualHours + } + workHours += tmpvirtualHours + } + worktimeBalance := workHours - targetHoursThisMonth + + typstDays, err := convertDaysToTypst(workDaysThisMonth, employee) + if err != nil { + slog.Warn("Failed to convert to days", slog.Any("error", err)) + return typstData{}, err + } + + metadata := typstMetadata{ + EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name), + TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)), + Overtime: helper.FormatDurationFill(worktimeBalance, true), + WorkTime: helper.FormatDurationFill(workHours, true), + Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true), + OvertimeTotal: "", + CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), + } + return typstData{Meta: metadata, Days: typstDays}, nil +} + func renderPDF(data []typstData) (bytes.Buffer, error) { var markup bytes.Buffer var output bytes.Buffer @@ -90,115 +185,6 @@ func renderPDF(data []typstData) (bytes.Buffer, error) { return output, nil } -func PDFCreateController(w http.ResponseWriter, r *http.Request) { - helper.RequiresLogin(Session, w, r) - switch r.Method { - case http.MethodGet: - user, err := models.GetUserFromSession(Session, r.Context()) - if err != nil { - log.Println("Error getting user!") - return - } - pp := paramParser.New(r.URL.Query()) - startDate := pp.ParseTimestampFallback("start_date", time.DateOnly, time.Now()) - var members []models.User = make([]models.User, 0) - output, err := createReports(user, members, startDate) - if err != nil { - slog.Warn("Could not create pdf report", slog.Any("Error", err)) - } - - w.Header().Set("Content-type", "application/pdf") - output.WriteTo(w) - w.WriteHeader(http.StatusOK) - - default: - http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) - } -} - -func createReports(user models.User, employes []models.User, startDate time.Time) (bytes.Buffer, error) { - if startDate.Day() > 1 { - startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1)) - } - endDate := startDate.AddDate(0, 1, -1) - return createEmployeReport(user, startDate, endDate) -} - -func createEmployeReport(employee models.User, startDate, endDate time.Time) (bytes.Buffer, error) { - targetHours := (employee.ArbeitszeitProWoche() / 5) * time.Duration(helper.GetWorkingDays(startDate, endDate)) - workingDays := models.GetDays(employee, startDate, endDate, false) - - slog.Debug("Baseline Working hours", "targetHours", targetHours.Hours()) - - var actualHours time.Duration - for _, day := range workingDays { - actualHours += day.GetWorktime(employee, models.WorktimeBaseDay, true) - } - worktimeBalance := actualHours - targetHours - - typstDays, err := convertDaysToTypst(workingDays, employee) - if err != nil { - log.Panicf("Failed to convert days!") - } - - metadata := typstMetadata{ - EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name), - TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)), - Overtime: helper.FormatDurationFill(worktimeBalance, true), - WorkTime: helper.FormatDurationFill(actualHours, true), - OvertimeTotal: "", - CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), - } - - return renderPDF([]typstData{{Meta: metadata, Days: typstDays}, {Meta: metadata, Days: typstDays}}) -} - -func PDFHandler(w http.ResponseWriter, r *http.Request) { - helper.RequiresLogin(Session, w, r) - startDate := time.Now() - - if startDate.Day() > 1 { - startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1)) - } - endDate := startDate.AddDate(0, 1, -1) - - user, err := models.GetUserFromSession(Session, r.Context()) - if err != nil { - log.Println("Error getting user!") - } - - //TODO: only accepted weeks - - weeks := models.GetDays(user, startDate, endDate, false) - var aggregatedOvertime, aggregatedWorkTime time.Duration - for _, day := range weeks { - aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseWeek, false) - aggregatedWorkTime += day.GetWorktime(user, models.WorktimeBaseWeek, true) - } - - typstDays, err := convertDaysToTypst(weeks, user) - if err != nil { - log.Panicf("Failed to convert days!") - } - metadata := typstMetadata{ - EmployeeName: fmt.Sprintf("%s %s", user.Vorname, user.Name), - TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)), - Overtime: helper.FormatDurationFill(aggregatedOvertime, true), - WorkTime: helper.FormatDurationFill(aggregatedWorkTime, true), - OvertimeTotal: "", - CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), - } - - output, err := renderPDF([]typstData{{Meta: metadata, Days: typstDays}}) - if err != nil { - slog.Warn("Could not create pdf report", slog.Any("Error", err)) - } - - w.Header().Set("Content-type", "application/pdf") - output.WriteTo(w) - w.WriteHeader(200) -} - type typstMetadata struct { TimeRange string `json:"time-range"` EmployeeName string `json:"employee-name"` @@ -217,12 +203,13 @@ type typstDayPart struct { } type typstDay struct { - Date string `json:"date"` - DayParts []typstDayPart `json:"day-parts"` - Worktime string `json:"worktime"` - Pausetime string `json:"pausetime"` - Overtime string `json:"overtime"` - IsFriday bool `json:"is-weekend"` + 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 { diff --git a/Backend/helper/paramParser/main.go b/Backend/helper/paramParser/main.go index 828b01f..dcce66d 100644 --- a/Backend/helper/paramParser/main.go +++ b/Backend/helper/paramParser/main.go @@ -5,6 +5,7 @@ import ( "log/slog" "net/url" "strconv" + "strings" "time" ) @@ -12,6 +13,30 @@ type ParamsParser struct { urlParams url.Values } +func (p ParamsParser) ParseStringListFallback(key string, delimiter string, fallback []string) []string { + if !p.urlParams.Has(key) { + return fallback + } + paramList := p.urlParams.Get(key) + list := strings.Split(paramList, delimiter) + return list +} + +func (p ParamsParser) ParseIntListFallback(key string, delimiter string, fallback []int) []int { + if !p.urlParams.Has(key) { + return fallback + } + paramList := p.urlParams.Get(key) + list := strings.Split(paramList, delimiter) + parsedList := make([]int, 0) + for _, item := range list { + if parsedItem, err := strconv.Atoi(item); err == nil { + parsedList = append(parsedList, parsedItem) + } + } + return parsedList +} + type NoValueError struct { Key string } diff --git a/Backend/helper/time.go b/Backend/helper/time.go index fcd739d..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 } diff --git a/Backend/models/absence.go b/Backend/models/absence.go index 1ff28e2..0d1beaa 100644 --- a/Backend/models/absence.go +++ b/Backend/models/absence.go @@ -86,77 +86,6 @@ func (a *Absence) GetTimes(u User, base WorktimeBase, includeKurzarbeit bool) (w return a.GetWorktime(u, base, includeKurzarbeit), a.GetPausetime(u, base, includeKurzarbeit), a.GetOvertime(u, base, includeKurzarbeit) } -func (a *Absence) GetWorktimeReal(u User, base WorktimeBase) time.Duration { - if a.AbwesenheitTyp.WorkTime <= 0 { - return 0 - } - switch base { - case WorktimeBaseDay: - return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100) - case WorktimeBaseWeek: - return u.ArbeitszeitProWocheFrac(0.2 * float32(a.AbwesenheitTyp.WorkTime) / 100) - } - return 0 -} -func (a *Absence) GetPausetimeReal(u User, base WorktimeBase) time.Duration { - return 0 -} - -func (a *Absence) GetOvertimeReal(u User, base WorktimeBase) time.Duration { - if a.AbwesenheitTyp.WorkTime > 0 { - return 0 - } - switch base { - case WorktimeBaseDay: - return -u.ArbeitszeitProTagFrac(1) - case WorktimeBaseWeek: - return -u.ArbeitszeitProWocheFrac(0.2) - } - return 0 -} - -func (a *Absence) GetWorktimeVirtual(u User, base WorktimeBase) time.Duration { - return a.GetWorktimeReal(u, base) -} - -func (a *Absence) GetPausetimeVirtual(u User, base WorktimeBase) time.Duration { - return a.GetPausetimeReal(u, base) -} - -func (a *Absence) GetOvertimeVirtual(u User, base WorktimeBase) time.Duration { - return a.GetOvertimeReal(u, base) -} - -func (a *Absence) GetTimesReal(u User, base WorktimeBase) (work, pause, overtime time.Duration) { - return a.GetWorktimeReal(u, base), a.GetPausetimeReal(u, base), a.GetOvertimeReal(u, base) -} - -func (a *Absence) GetTimesVirtual(u User, base WorktimeBase) (work, pause, overtime time.Duration) { - return a.GetWorktimeVirtual(u, base), a.GetPausetimeVirtual(u, base), a.GetOvertimeVirtual(u, base) -} - -func (a *Absence) TimeWorkVirtual(u User) time.Duration { - return a.TimeWorkReal(u) -} - -func (a *Absence) TimeWorkReal(u User) time.Duration { - if a.AbwesenheitTyp.WorkTime > 1 { - return time.Duration(u.ArbeitszeitPerTag * float32(time.Hour)).Round(time.Minute) - } - return 0 -} - -func (a *Absence) TimePauseReal(u User) (work, pause time.Duration) { - return 0, 0 -} - -func (a *Absence) TimeOvertimeReal(u User) time.Duration { - if a.AbwesenheitTyp.WorkTime > 1 { - return 0 - } - return -u.ArbeitszeitProTag() -} - func (a *Absence) ToString() string { return "Abwesenheit" } diff --git a/Backend/models/absence_test.go b/Backend/models/absence_test.go index 7e40dea..235579b 100644 --- a/Backend/models/absence_test.go +++ b/Backend/models/absence_test.go @@ -52,7 +52,7 @@ func TestCalcRealWorkTimeDayAbsence(t *testing.T) { t.Run("Calc Absence Worktime: "+tc.absenceType.Name, func(t *testing.T) { var testCase = testAbsence testCase.AbwesenheitTyp = tc.absenceType - workTime := testCase.GetWorktimeReal(testUser, models.WorktimeBaseDay) + 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)) } @@ -83,7 +83,7 @@ func TestCalcRealWorkTimeWeekAbsence(t *testing.T) { t.Run("Calc Absence Worktime: "+tc.absenceType.Name, func(t *testing.T) { var testCase = testAbsence testCase.AbwesenheitTyp = tc.absenceType - workTime := testCase.GetWorktimeReal(testUser, models.WorktimeBaseWeek) + 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/user.go b/Backend/models/user.go index 8f654e0..1e87b3d 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -10,6 +10,7 @@ import ( "time" "github.com/alexedwards/scs/v2" + "github.com/lib/pq" ) type User struct { @@ -173,6 +174,38 @@ 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;`)) diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index e82bd5a..f6e88d1 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -30,15 +30,19 @@ const ( WorktimeBaseDay WorktimeBase = "day" ) +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() { + if includeKurzarbeit && d.IsKurzArbeit() && len(d.Bookings) > 0 { return d.kurzArbeitAbsence.GetWorktime(u, base, true) } work, pause := calcWorkPause(d.Bookings) work, pause = correctWorkPause(work, pause) if (d.worktimeAbsece != Absence{}) { - work += d.worktimeAbsece.GetWorktimeReal(u, base) + work += d.worktimeAbsece.GetWorktime(u, base, false) } return work.Round(time.Minute) } @@ -124,12 +128,12 @@ func (d *WorkDay) Date() time.Time { func (d *WorkDay) GenerateKurzArbeitBookings(u User) (time.Time, time.Time) { var timeFrom, timeTo time.Time - if d.workTime >= u.ArbeitszeitProTag() { + if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() { return timeFrom, timeTo } timeFrom = d.Bookings[len(d.Bookings)-1].Timestamp.Add(time.Minute) - timeTo = timeFrom.Add(u.ArbeitszeitProTag() - d.workTime) + 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 diff --git a/Backend/templates/timePage.templ b/Backend/templates/timePage.templ index 297d2d9..92cb0a2 100644 --- a/Backend/templates/timePage.templ +++ b/Backend/templates/timePage.templ @@ -137,7 +137,7 @@ templ defaultDayComponent(day models.IWorkDay) { if len(workDay.Bookings) < 1 {

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

} - if workDay.IsKurzArbeit() { + if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 { @absenceComponent(workDay.GetKurzArbeit(), true) } for _, booking := range workDay.Bookings { diff --git a/Backend/templates/timePage_templ.go b/Backend/templates/timePage_templ.go index 4bfba6c..506f94c 100644 --- a/Backend/templates/timePage_templ.go +++ b/Backend/templates/timePage_templ.go @@ -436,7 +436,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if workDay.IsKurzArbeit() { + if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 { templ_7745c5c3_Err = absenceComponent(workDay.GetKurzArbeit(), true).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err diff --git a/DocumentCreator/templates/abrechnung.typ b/DocumentCreator/templates/abrechnung.typ index ee71ebc..0735264 100644 --- a/DocumentCreator/templates/abrechnung.typ +++ b/DocumentCreator/templates/abrechnung.typ @@ -37,31 +37,35 @@ [Zeitraum: #meta.TimeRange] table( - columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, .875fr, 1.25fr), + 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], [Pause], [Überstunden] + [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 not day.DayParts.first().IsWorkDay{ + }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 { ( - [#Zeit.BookingFrom], - [#Zeit.BookingTo], - [#Zeit.WorkType], + if Zeit.IsWorkDay{ + ( + table.cell()[#Zeit.BookingFrom], + table.cell()[#Zeit.BookingTo], + table.cell()[#Zeit.WorkType], + ) + }else{ + (table.cell(colspan: 3)[#Zeit.WorkType],) + } ) }, ) @@ -85,9 +89,9 @@ 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 :],table.cell(align: left)[#meta.OvertimeTotal], + [Überstunden lfd. :],table.cell(align: left)[#meta.OvertimeTotal], table.hline(start: 0, end: 2), - ) } -- 2.49.1 From 588bf908c6f35f01b88ecd0268cad10b70c45f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 12 Dec 2025 12:58:41 +0100 Subject: [PATCH 22/35] fixed tests --- Backend/models/absence.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Backend/models/absence.go b/Backend/models/absence.go index 0d1beaa..0086337 100644 --- a/Backend/models/absence.go +++ b/Backend/models/absence.go @@ -54,11 +54,15 @@ func (a *Absence) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool) 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) } -- 2.49.1 From a1b225478aef5f5aced46303a168f6323fda139f Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 12 Dec 2025 14:13:24 +0100 Subject: [PATCH 23/35] fixed sonarqube issue --- Backend/models/user.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Backend/models/user.go b/Backend/models/user.go index 1e87b3d..373ae27 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "log" + "log/slog" "time" "github.com/alexedwards/scs/v2" @@ -62,7 +63,6 @@ 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 { - fmt.Printf("Error preparing query statement %v\n", err) return users, err } defer qStr.Close() @@ -90,7 +90,6 @@ 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() @@ -136,7 +135,7 @@ func (u *User) ArbeitszeitProWocheFrac(fraction float32) 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() @@ -210,7 +209,7 @@ 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() @@ -302,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) -- 2.49.1 From c7f8595474e24bf3e362dfc67d1c95453a3c66b7 Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 12 Dec 2025 14:28:49 +0100 Subject: [PATCH 24/35] updated github workflow for document creator --- .gitea/workflows/build.yaml | 89 +++++++++++++++++------------------- WIR-typst/main.typ | 63 ------------------------- WIR-typst/template.pdf | Bin 2193 -> 0 bytes 3 files changed, 42 insertions(+), 110 deletions(-) delete mode 100644 WIR-typst/main.typ delete mode 100644 WIR-typst/template.pdf diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index e112b79..42f44e2 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -4,55 +4,13 @@ on: push: tags: - "*" + branches: + - main jobs: - testing: - name: Run Go Tests - runs-on: ubuntu-latest - services: - postgres: - image: postgres:16 - env: - POSTGRES_USER: root - POSTGRES_PASSWORD: password - POSTGRES_DB: arbeitszeitmessung - env: - POSTGRES_HOST: postgres - POSTGRES_USER: root - POSTGRES_PASSWORD: password - POSTGRES_DB: arbeitszeitmessung - POSTGRES_PORT: 5432 - RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup go - uses: actions/setup-go@v5 - with: - go-version-file: Backend/go.mod - - uses: https://gitea.com/actions/go-hashfiles@v0.0.1 - id: hash-go - with: - patterns: | - go.mod - go.sum - - name: cache go - id: cache-go - uses: actions/cache@v4 - with: - path: |- - /go_path - /go_cache - key: arbeitszeitmessung-${{ steps.hash-go.outputs.hash }} - restore-keys: |- - arbeitszeitmessung- - - name: Run Go Tests - run: cd Backend && go test ./... - build: + webserver: name: Build Go Image and Upload runs-on: ubuntu-latest - needs: [testing] steps: - name: Checkout uses: actions/checkout@v4 @@ -66,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 Go Image and Upload + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: git.letsstein.de + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: git.letsstein.de/tom/arbeitszeitmessung-doc-creator tags: | - git.letsstein.de/tom/arbeitszeitmessung-webserver:latest - git.letsstein.de/tom/arbeitszeitmessung-webserver:${{ 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/WIR-typst/main.typ b/WIR-typst/main.typ deleted file mode 100644 index 2f9effa..0000000 --- a/WIR-typst/main.typ +++ /dev/null @@ -1,63 +0,0 @@ -#set page("a4") -#set text(font: "Lato") - -= Stunden - -== Kim Mustermensch - -Zeitraum: 01.10.2025 - 31.10.2025 - -Arbeitszeit: 136h 19min - -Überstunden: -39h 41min - - - - - -// #show table.cell: it => { -// if it.y == 0 { -// set text(white) -// strong(it) -// } else if it.body == [] { -// // Replace empty cells with 'N/A' -// pad(..it.inset)[0min] - -// } else { -// it -// } -// } - -#let subgrid(body) = { - table.cell(colspan: 3, inset: 0em)[ - #table( - columns: (1fr, 1fr, 1fr), - gutter: 0em, - stroke: black, - [..#body] - ) - ] -} - - - "01.09.2025", - "08:07", - "16:28", - "Büro", - "7h 51min", - "30min", - "-9min", - "02.09.2025", - // return work, pause, overtime -table.cell(colspan: 3, inset: 0em)[#table( - columns: (1fr, 1fr, 1fr), - gutter: 0em, - stroke: black, - [08:12], [16:24], [Büro], - [16:30], [17:24], [Homeoffice] - )], - "6h", - "0min", - "-1h 15min" - -) \ No newline at end of file diff --git a/WIR-typst/template.pdf b/WIR-typst/template.pdf deleted file mode 100644 index db53c7ae6ae2d912d5f666f3260dc937145546e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2193 zcma)8&2Hm15Z*&iTJ#+V6#)_ywnWKtY(dsyW39VJ<6Xlxwu`1Y&=Rf4kwk%_VI`+N zLLXo+z4aY>=nEC-D|ATz$xgDZE|xxiGn{Y8neo{;7&LJ6Qak$<9!Z%hw;1 z2qKi}nH+`b?Coo@x1R$qp95tM4%>hJIA79GAn8=4cuWK+&5?AdfOnPnVJbj2lI|4L zXDQ>;f`k-xcGv*Yjyw-SszoJNH+$8gfdT8+GAb!iJ6%McnsvU^dn~fdkNek4Fo45)8Ix@YU1rgeY9YSg+U3wcTVq zHgLDwHB8H}tOhU|kc%EHf%n0Mk5Tjz(YSX4w&aHjcl}7e)~uEQ(q-jzWP2 z2*m~_WIik0{^okk7)$;74fBlCRY@O>+<=8EseKsuv*9?JT&|Z(YZ&&UFka3HGvD@l z%J!JLH6G@rAbCjthjESSMWMT3JR`yY-wyt1@ Date: Fri, 12 Dec 2025 15:07:57 +0100 Subject: [PATCH 25/35] updated pdf form to send user to pdf generator --- Backend/endpoints/pdf-create.go | 12 +- Backend/models/workDay.go | 1 - Backend/templates/headerComponent_templ.go | 2 +- Backend/templates/pages_templ.go | 2 +- Backend/templates/pdf.templ | 19 +-- Backend/templates/pdf_templ.go | 134 +++++++++++++-------- Backend/templates/presencePage_templ.go | 2 +- Backend/templates/teamComponents_templ.go | 2 +- Backend/templates/timeComponents_templ.go | 2 +- Backend/templates/timePage_templ.go | 2 +- 10 files changed, 106 insertions(+), 72 deletions(-) diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index 5c74326..c98232f 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -96,10 +96,14 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) { if err != nil { slog.Warn("Could not create pdf report", slog.Any("Error", err)) } - - w.Header().Set("Content-type", "application/pdf") - output.WriteTo(w) - w.WriteHeader(http.StatusOK) + switch pp.ParseStringFallback("output", "render") { + case "render": + w.Header().Set("Content-type", "application/pdf") + output.WriteTo(w) + w.WriteHeader(http.StatusOK) + case "download": + panic("Not implemented") + } default: http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index f6e88d1..e8f1bd6 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -231,7 +231,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 diff --git a/Backend/templates/headerComponent_templ.go b/Backend/templates/headerComponent_templ.go index b13c034..39e8195 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.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/pages_templ.go b/Backend/templates/pages_templ.go index 7753eaa..6e32b44 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.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/pdf.templ b/Backend/templates/pdf.templ index 332ab33..7bed3a1 100644 --- a/Backend/templates/pdf.templ +++ b/Backend/templates/pdf.templ @@ -10,7 +10,7 @@ import ( templ PDFForm(teamMembers []models.User) { @Base() @headerComponent() -
+

PDF Abrechnung erstellen

@@ -18,7 +18,7 @@ templ PDFForm(teamMembers []models.User) {
Zeitraum wählen
- +
@@ -30,7 +30,7 @@ templ PDFForm(teamMembers []models.User) {
for _, member := range teamMembers { - @CheckboxComponent(fmt.Sprintf("pdf-%d", member.PersonalNummer), fmt.Sprintf("%s %s", member.Vorname, member.Name)) + @CheckboxComponent(member.PersonalNummer, fmt.Sprintf("%s %s", member.Vorname, member.Name)) }
@@ -38,17 +38,20 @@ templ PDFForm(teamMembers []models.User) {
PDFs Bündeln
- - + +
-
+ } -templ CheckboxComponent(id, label string) { +templ CheckboxComponent(pNr int, label string) { + {{ + id := fmt.Sprintf("pdf-%d", pNr) + }}
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -257,9 +285,9 @@ func ColorDuration(d time.Duration, classes string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var9 := templ.GetChildren(ctx) - if templ_7745c5c3_Var9 == nil { - templ_7745c5c3_Var9 = templ.NopComponent + templ_7745c5c3_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent } ctx = templ.ClearChildren(ctx) @@ -267,38 +295,38 @@ func ColorDuration(d time.Duration, classes string) templ.Component { if d.Abs() < time.Minute { color = "text-neutral-300" } - var templ_7745c5c3_Var10 = []any{color + " " + classes} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...) + 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, 12, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, 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: 132, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pdf.templ`, Line: 135, Col: 72} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + _, 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, 14, "

") + 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.go b/Backend/templates/presencePage_templ.go index c7eca82..b2820e0 100644 --- a/Backend/templates/presencePage_templ.go +++ b/Backend/templates/presencePage_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.924 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index 8cdc91c..b23c68b 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.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/timeComponents_templ.go b/Backend/templates/timeComponents_templ.go index d14abca..09fd2c6 100644 --- a/Backend/templates/timeComponents_templ.go +++ b/Backend/templates/timeComponents_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.924 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/timePage_templ.go b/Backend/templates/timePage_templ.go index 506f94c..1a87a94 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.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. -- 2.49.1 From 82eb8018a6306d99b5e9dab05beadc7764329322 Mon Sep 17 00:00:00 2001 From: tom Date: Tue, 16 Dec 2025 07:02:17 +0100 Subject: [PATCH 26/35] updated pdf renderer to support zipped output --- .gitea/workflows/build.yaml | 4 +- Backend/endpoints/pdf-create.go | 103 +++++++++++++++++++++++++++----- Backend/go.mod | 2 + Backend/go.sum | 64 ++++++++++++++++++++ Backend/main.go | 1 + Backend/models/user.go | 16 +++++ 6 files changed, 174 insertions(+), 16 deletions(-) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 42f44e2..d2f0e60 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -9,7 +9,7 @@ on: jobs: webserver: - name: Build Go Image and Upload + name: Build Webserver runs-on: ubuntu-latest steps: - name: Checkout @@ -40,7 +40,7 @@ jobs: context: Backend tags: ${{ steps.meta.outputs.tags }} document-creator: - name: Build Go Image and Upload + name: Build Document Creator runs-on: ubuntu-latest steps: - name: Checkout diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index c98232f..7b37bc9 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -4,6 +4,7 @@ import ( "arbeitszeitmessung/helper" "arbeitszeitmessung/helper/paramParser" "arbeitszeitmessung/models" + "archive/zip" "bytes" "fmt" "log" @@ -15,6 +16,7 @@ import ( ) 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 @@ -92,17 +94,43 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) { return } - output, err := createReports(user, employes, startDate) - if err != nil { - slog.Warn("Could not create pdf report", slog.Any("Error", err)) + 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": - panic("Not implemented") + 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: @@ -110,7 +138,7 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) { } } -func createReports(user models.User, employes []models.User, startDate time.Time) (bytes.Buffer, error) { +func createReports(employes []models.User, startDate time.Time) []typstData { startDate = helper.GetFirstOfMonth(startDate) endDate := startDate.AddDate(0, 1, -1) @@ -122,7 +150,7 @@ func createReports(user models.User, employes []models.User, startDate time.Time employeData = append(employeData, data) } } - return renderPDF(employeData) + return employeData } func createEmployeReport(employee models.User, startDate, endDate time.Time) (typstData, error) { @@ -158,13 +186,17 @@ func createEmployeReport(employee models.User, startDate, endDate time.Time) (ty OvertimeTotal: "", CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"), } - return typstData{Meta: metadata, Days: typstDays}, nil + return typstData{Meta: metadata, Days: typstDays, FileName: fmt.Sprintf("%s_%s.pdf", startDate.Format(FILE_YEAR_MONTH), employee.Name)}, nil } -func renderPDF(data []typstData) (bytes.Buffer, error) { +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 } @@ -179,16 +211,58 @@ func renderPDF(data []typstData) (bytes.Buffer, error) { `) // Compile the prepared markup with Typst and write the result it into `output.pdf`. - - typstCLI := typst.DockerExec{ - ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"), - } if err := typstCLI.Compile(&markup, &output, nil); err != nil { return output, err } return output, nil } +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"` @@ -217,6 +291,7 @@ type typstDay struct { } type typstData struct { - Meta typstMetadata `json:"meta"` - Days []typstDay `json:"days"` + Meta typstMetadata `json:"meta"` + Days []typstDay `json:"days"` + FileName string } diff --git a/Backend/go.mod b/Backend/go.mod index 518cfc4..f11e0c9 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -8,6 +8,8 @@ require github.com/a-h/templ v0.3.943 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 diff --git a/Backend/go.sum b/Backend/go.sum index 95a0e66..9cfab85 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -8,6 +8,10 @@ github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY= github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= +github.com/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= @@ -40,6 +44,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= @@ -59,9 +64,22 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN 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= @@ -72,19 +90,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/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/main.go b/Backend/main.go index e74d7bb..4832dc0 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -48,6 +48,7 @@ func main() { server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler)) 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) diff --git a/Backend/models/user.go b/Backend/models/user.go index 373ae27..5cfb559 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -330,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 { -- 2.49.1 From 177fbdeb3f76fed859ea3ecf25eb13d14f532eac Mon Sep 17 00:00:00 2001 From: tom Date: Tue, 16 Dec 2025 07:02:52 +0100 Subject: [PATCH 27/35] adding feiertage in db --- Backend/endpoints/auto-feiertage.go | 14 ++++++++++++++ DB/initdb/01_schema.sql | 8 +++++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 Backend/endpoints/auto-feiertage.go diff --git a/Backend/endpoints/auto-feiertage.go b/Backend/endpoints/auto-feiertage.go new file mode 100644 index 0000000..7079a36 --- /dev/null +++ b/Backend/endpoints/auto-feiertage.go @@ -0,0 +1,14 @@ +package endpoints + +import ( + "log/slog" + "net/http" + "time" + + "github.com/wlbr/feiertage" +) + +func FeiertagsHandler(w http.ResponseWriter, r *http.Request) { + feiertage := feiertage.Sachsen(time.Now().Year(), true) + slog.Info("Hier sind die Feiertage", "Feiertage", feiertage) +} diff --git a/DB/initdb/01_schema.sql b/DB/initdb/01_schema.sql index b434f4c..3654cf7 100755 --- a/DB/initdb/01_schema.sql +++ b/DB/initdb/01_schema.sql @@ -103,9 +103,15 @@ CREATE TABLE "s_abwesenheit_typen" ( "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, +); + -- Adds crypto extension CREATE EXTENSION IF NOT EXISTS pgcrypto; -- 2.49.1 From f562ef2a332995e0004cb0a26ddadb5951330caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 19 Dec 2025 09:15:58 +0100 Subject: [PATCH 28/35] added public holidays + updated templ to v0.3.960 --- Backend/endpoints/pdf-create.go | 9 ++-- Backend/go.mod | 2 +- Backend/go.sum | 2 + Backend/helper/paramParser/main.go | 5 +- Backend/models/publicHoliday.go | 53 ++++++++++++++++++++ Backend/templates/headerComponent_templ.go | 2 +- Backend/templates/pages_templ.go | 3 +- Backend/templates/pdf_templ.go | 4 +- Backend/templates/presencePage_templ.go | 2 +- Backend/templates/teamComponents_templ.go | 7 +-- Backend/templates/timeComponents_templ.go | 4 +- Backend/templates/timePage_templ.go | 8 +-- DB/initdb/01_schema.sql | 7 ++- migrations/20251217215955_feiertage.down.sql | 6 +++ migrations/20251217215955_feiertage.up.sql | 11 ++++ migrations/atlas.sum | 3 +- 16 files changed, 95 insertions(+), 33 deletions(-) create mode 100644 Backend/models/publicHoliday.go create mode 100644 migrations/20251217215955_feiertage.down.sql create mode 100644 migrations/20251217215955_feiertage.up.sql diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index 7b37bc9..443a2a1 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -143,9 +143,9 @@ func createReports(employes []models.User, startDate time.Time) []typstData { endDate := startDate.AddDate(0, 1, -1) var employeData []typstData - for _, employe := range employes { - if data, err := createEmployeReport(employe, startDate, endDate); err != nil { - slog.Warn("Error when creating employeReport", slog.Any("user", employe), slog.Any("error", err)) + 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) } @@ -154,7 +154,8 @@ func createReports(employes []models.User, startDate time.Time) []typstData { } func createEmployeReport(employee models.User, startDate, endDate time.Time) (typstData, error) { - targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)) + 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()) diff --git a/Backend/go.mod b/Backend/go.mod index f11e0c9..3c70ca4 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -4,7 +4,7 @@ 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 diff --git a/Backend/go.sum b/Backend/go.sum index 9cfab85..0c9f849 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -6,6 +6,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo 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= diff --git a/Backend/helper/paramParser/main.go b/Backend/helper/paramParser/main.go index dcce66d..8dd6586 100644 --- a/Backend/helper/paramParser/main.go +++ b/Backend/helper/paramParser/main.go @@ -26,10 +26,9 @@ func (p ParamsParser) ParseIntListFallback(key string, delimiter string, fallbac if !p.urlParams.Has(key) { return fallback } - paramList := p.urlParams.Get(key) - list := strings.Split(paramList, delimiter) + paramList := p.urlParams[key] parsedList := make([]int, 0) - for _, item := range list { + for _, item := range paramList { if parsedItem, err := strconv.Atoi(item); err == nil { parsedList = append(parsedList, parsedItem) } diff --git a/Backend/models/publicHoliday.go b/Backend/models/publicHoliday.go new file mode 100644 index 0000000..351ae6a --- /dev/null +++ b/Backend/models/publicHoliday.go @@ -0,0 +1,53 @@ +package models + +import "time" + +type PublicHoliday struct { + name string + date time.Time +} + +func GetHolidaysFromTo(tsFrom, tsTo time.Time) ([]PublicHoliday, error) { + return make([]PublicHoliday, 0), nil +} + +// Interface implementation +func (p *PublicHoliday) Date() time.Time { + return time.Now() +} + +func (p *PublicHoliday) ToString() string { + return "" +} + +func (p *PublicHoliday) IsWorkDay() bool { + return false +} + +func (p *PublicHoliday) IsKurzArbeit() bool { + return false +} + +func (p *PublicHoliday) GetDayProgress(User) int8 { + return 0 +} + +func (p *PublicHoliday) RequiresAction() bool { + return false +} + +func (p *PublicHoliday) GetWorktime(User, WorktimeBase, bool) time.Duration { + return 0 +} + +func (p *PublicHoliday) GetPausetime(User, WorktimeBase, bool) time.Duration { + return 0 +} + +func (p *PublicHoliday) GetTimes(User, WorktimeBase, bool) (work, pause, overtime time.Duration) { + return 0, 0, 0 +} + +func (p *PublicHoliday) GetOvertime(User, WorktimeBase, bool) time.Duration { + return 0 +} diff --git a/Backend/templates/headerComponent_templ.go b/Backend/templates/headerComponent_templ.go index 39e8195..aa67d81 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.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/pages_templ.go b/Backend/templates/pages_templ.go index 6e32b44..933f53c 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.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -116,7 +116,6 @@ func SettingsPage(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 { diff --git a/Backend/templates/pdf_templ.go b/Backend/templates/pdf_templ.go index ffc7c06..b45c4c7 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.943 +// templ: version: v0.3.960 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -130,7 +130,6 @@ func CheckboxComponent(pNr int, label string) templ.Component { templ_7745c5c3_Var5 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - id := fmt.Sprintf("pdf-%d", pNr) templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + 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 } @@ -209,18 +236,18 @@ func statusCheckMark(status models.WeekStatus, target models.WeekStatus) templ.C }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var8 := templ.GetChildren(ctx) - if templ_7745c5c3_Var8 == nil { - templ_7745c5c3_Var8 = templ.NopComponent + templ_7745c5c3_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if status >= target { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -229,63 +256,6 @@ func statusCheckMark(status models.WeekStatus, target models.WeekStatus) templ.C }) } -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 LogoutButton() templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context @@ -302,12 +272,12 @@ func LogoutButton() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var10 := templ.GetChildren(ctx) - if templ_7745c5c3_Var10 == nil { - templ_7745c5c3_Var10 = 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, 19, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/reportPage.templ b/Backend/templates/reportPage.templ new file mode 100644 index 0000000..df2bab2 --- /dev/null +++ b/Backend/templates/reportPage.templ @@ -0,0 +1,24 @@ +package templates + +import "arbeitszeitmessung/models" + +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) + } +
+} diff --git a/Backend/templates/reportPage_templ.go b/Backend/templates/reportPage_templ.go new file mode 100644 index 0000000..6a97c20 --- /dev/null +++ b/Backend/templates/reportPage_templ.go @@ -0,0 +1,70 @@ +// 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" + +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 + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/Backend/templates/teamComponents.templ b/Backend/templates/teamComponents.templ index 26501a4..9790ef7 100644 --- a/Backend/templates/teamComponents.templ +++ b/Backend/templates/teamComponents.templ @@ -9,9 +9,7 @@ import ( ) templ weekPicker(weekStart time.Time) { - {{ - year, kw := weekStart.ISOWeek() - }} + {{ year, kw := weekStart.ISOWeek() }}
} -templ weekDayComponent(user models.User, day models.WorkDay) { - // {{ work, pause, _ := day.GetAllWorkTimesReal(user) }} -
- // @timeGaugeComponent(day.GetWorkDayProgress(user), false, day.RequiresAction()) -
- if !day.RequiresAction() { +templ workDayWeekComponent(workDay *models.WorkDay, u models.User) { + if !workDay.RequiresAction() { +
+ + switch { + case !workDay.TimeFrom.Equal(workDay.TimeTo): + { workDay.TimeFrom.Format("15:04") } + - + { workDay.TimeTo.Format("15:04") } + default: +

Keine Anwesenheit

}
-
+ } else { +

Bitte anpassen

+ } } templ workWeekComponent(week models.WorkWeek, onlyAccept bool) { diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index cbf8a46..e65c224 100644 --- a/Backend/templates/teamComponents_templ.go +++ b/Backend/templates/teamComponents_templ.go @@ -45,7 +45,7 @@ func weekPicker(weekStart time.Time) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(weekStart.Format(time.DateOnly)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 16, Col: 98} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 14, Col: 98} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -75,7 +75,7 @@ func weekPicker(weekStart time.Time) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d, %d", kw, year)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 22, Col: 69} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 20, Col: 69} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -152,7 +152,7 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatGermanDayOfWeek(day.Date())) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 108} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 33, Col: 108} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -165,7 +165,7 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 152} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 33, Col: 152} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -175,109 +175,93 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if day.IsWorkDay() { + work, pause, _ := day.GetTimes(u, models.WorktimeBaseDay, false) + 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: 36, Col: 59} + } + _, 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: 37, Col: 65} + } + _, 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 day.Type() { + case models.DayTypeWorkday: workDay, _ := day.(*models.WorkDay) - work, pause, _ := workDay.GetTimes(u, models.WorktimeBaseDay, false) - 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, "") + templ_7745c5c3_Err = workDayWeekComponent(workDay, u).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case models.DayTypeCompound: + for _, c := range day.(*models.CompoundDay).DayParts { + switch c.Type() { + case models.DayTypeWorkday: + workDay, _ := c.(*models.WorkDay) + templ_7745c5c3_Err = workDayWeekComponent(workDay, u).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + 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")) + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(c.ToString()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 50, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 50, Col: 27} } _, 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, " - ") + 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, "
") + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") 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) + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(day.ToString()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 64, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 54, Col: 26} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + _, 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, 24, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -285,7 +269,7 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component }) } -func weekDayComponent(user models.User, day models.WorkDay) templ.Component { +func workDayWeekComponent(workDay *models.WorkDay, u 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 { @@ -301,20 +285,63 @@ func weekDayComponent(user models.User, day models.WorkDay) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var14 := templ.GetChildren(ctx) - if templ_7745c5c3_Var14 == nil { - templ_7745c5c3_Var14 = templ.NopComponent + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 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 + if !workDay.RequiresAction() { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + switch { + case !workDay.TimeFrom.Equal(workDay.TimeTo): + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, 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: 66, Col: 45} + } + _, 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, 24, " - ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, 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: 68, Col: 43} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

Keine Anwesenheit

") + 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 + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

Bitte anpassen

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } return nil }) @@ -336,19 +363,19 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var15 := templ.GetChildren(ctx) - if templ_7745c5c3_Var15 == nil { - templ_7745c5c3_Var15 = templ.NopComponent + templ_7745c5c3_Var16 := templ.GetChildren(ctx) + if templ_7745c5c3_Var16 == nil { + templ_7745c5c3_Var16 = 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, 28, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if !onlyAccept { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -356,43 +383,43 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { 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, 31, "
") 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, " ") + 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) + templ_7745c5c3_Var17, 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: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 90, Col: 53} } _, 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, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, 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: 90, Col: 72} + } + _, 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, 34, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if !onlyAccept { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -400,7 +427,7 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "Gesendet ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Gesendet ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -408,12 +435,12 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Akzeptiert
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "Akzeptiert
") 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, 38, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -421,33 +448,33 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { 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: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

Arbeitszeit: ") 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))) + templ_7745c5c3_Var19, 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: 111, Col: 90} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 107, Col: 79} } _, 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, "

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

Überstunden: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, 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: 108, Col: 90} + } + _, 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, 41, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -457,30 +484,30 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if onlyAccept { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "

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

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)) + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 120, Col: 86} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -488,12 +515,12 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -502,124 +529,124 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { if !onlyAccept { method = "send" } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if onlyAccept { if week.Status == models.WeekStatusDifferences { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "

Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen

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

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, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, " class=\"btn\">Bestätigen") 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

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

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

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

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

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

an Vorgesetzten senden

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

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

an Vorgesetzten gesendet

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

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

vom Vorgesetzten bestätigt

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

vom Vorgesetzten bestätigt

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " Korrigieren Korrigieren = models.WeekStatusSent || week.RequiresAction() { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " disabled") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " disabled") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " type=\"submit\" class=\"btn\">Senden") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, " type=\"submit\" class=\"btn\">Senden") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -643,53 +670,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_Var25 := templ.GetChildren(ctx) + if templ_7745c5c3_Var25 == nil { + templ_7745c5c3_Var25 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
") 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, 68, "
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, 69, "
Abwesend
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "

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

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

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 170, Col: 33} + } + _, 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, 72, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/timeComponents.templ b/Backend/templates/timeComponents.templ index 9fad41d..e2f0d62 100644 --- a/Backend/templates/timeComponents.templ +++ b/Backend/templates/timeComponents.templ @@ -77,12 +77,9 @@ templ absenceComponent(a *models.Absence, isKurzarbeit bool) { } }}
- - - - + @absentInput(a)

- { a.AbwesenheitTyp.Name } + { a.ToString() } if a.IsMultiDay() { bis { a.DateTo.Format("02.01.2006") } } @@ -94,6 +91,13 @@ templ absenceComponent(a *models.Absence, isKurzarbeit bool) {

} +templ absentInput(a *models.Absence) { + + + + +} + templ newBookingComponent(d *models.WorkDay) { ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func absentInput(a *models.Absence) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_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, 25, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -434,58 +471,58 @@ 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_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, 28, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -509,74 +546,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_Var27 := templ.GetChildren(ctx) + if templ_7745c5c3_Var27 == nil { + templ_7745c5c3_Var27 = 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_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs("booking_" + strconv.Itoa(booking.CounterId)) + templ_7745c5c3_Var28, 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: 70} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 123, Col: 91} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" type=\"time\" value=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" type=\"time\" value=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(booking.GetBookingType()) + templ_7745c5c3_Var30, 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: 121, Col: 29} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 124, Col: 126} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" 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_Var31 string + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(booking.GetBookingType()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timeComponents.templ`, Line: 125, Col: 29} + } + _, 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, 41, "

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

submitted

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

submitted

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -600,12 +637,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_Var32 := templ.GetChildren(ctx) + if templ_7745c5c3_Var32 == nil { + templ_7745c5c3_Var32 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
Fehler
Arbeitszeit unter regulär
Arbeitszeit vollständig
Überstunden
Keine Buchungen
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
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 3e8723c..d0dd450 100644 --- a/Backend/templates/timePage.templ +++ b/Backend/templates/timePage.templ @@ -87,7 +87,7 @@ 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" } }} @@ -100,9 +100,8 @@ templ defaultDayComponent(day models.IWorkDay) {

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

Bitte anpassen

@@ -114,7 +113,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) } @@ -130,20 +129,25 @@ templ defaultDayComponent(day models.IWorkDay) { switch day.Type() { case models.DayTypeWorkday: {{ workDay, _ := day.(*models.WorkDay) }} + @workdayComponent(workDay) @newAbsenceComponent() - if len(workDay.Bookings) < 1 { -

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

- } - if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 { - @absenceComponent(workDay.GetKurzArbeit(), true) - } - for _, booking := range workDay.Bookings { - @bookingComponent(booking) - } - @newBookingComponent(workDay) case models.DayTypeAbsence: {{ absentDay, _ := day.(*models.Absence) }} @absenceComponent(absentDay, false) + case models.DayTypeCompound: + for _, c := range day.(*models.CompoundDay).DayParts { + switch c.Type() { + case models.DayTypeWorkday: + {{ workDay, _ := c.(*models.WorkDay) }} + @workdayComponent(workDay) + @newAbsenceComponent() + case models.DayTypeAbsence: + {{ absentDay, _ := c.(*models.Absence) }} + @absenceComponent(absentDay, false) + default: +

{ c.ToString() }

+ } + } default:

{ day.ToString() }

} @@ -156,9 +160,19 @@ templ defaultDayComponent(day models.IWorkDay) { } -templ absentInput(a models.Absence) { - - - - +templ workdayComponent(workDay *models.WorkDay) { + if len(workDay.Bookings) < 1 { +

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

+ } + if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 { + @absenceComponent(workDay.GetKurzArbeit(), true) + } + for _, booking := range workDay.Bookings { + @bookingComponent(booking) + } + @newBookingComponent(workDay) +} + +templ holidayComponent(d models.IWorkDay) { +

{ d.ToString() }

} diff --git a/Backend/templates/timePage_templ.go b/Backend/templates/timePage_templ.go index d6c1520..3484084 100644 --- a/Backend/templates/timePage_templ.go +++ b/Backend/templates/timePage_templ.go @@ -232,7 +232,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { 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"} @@ -292,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.GetTimes(user, models.WorktimeBaseDay, true) - work = workDay.GetWorktime(user, models.WorktimeBaseDay, false) + 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 { @@ -309,7 +308,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(work)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 112, Col: 155} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 111, Col: 155} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -332,7 +331,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(pause)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 115, Col: 173} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 114, Col: 173} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -347,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 @@ -355,7 +354,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatDuration(overtime)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 120, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 119, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -388,7 +387,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("time-" + day.Date().Format(time.DateOnly)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 129, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 128, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { @@ -414,7 +413,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { switch day.Type() { case models.DayTypeWorkday: workDay, _ := day.(*models.WorkDay) - templ_7745c5c3_Err = newAbsenceComponent().Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = workdayComponent(workDay).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -422,33 +421,7 @@ 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, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - 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, 36, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = newBookingComponent(workDay).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = newAbsenceComponent().Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -458,17 +431,60 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + case models.DayTypeCompound: + for _, c := range day.(*models.CompoundDay).DayParts { + switch c.Type() { + case models.DayTypeWorkday: + workDay, _ := c.(*models.WorkDay) + templ_7745c5c3_Err = workdayComponent(workDay).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = newAbsenceComponent().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case models.DayTypeAbsence: + absentDay, _ := c.(*models.Absence) + templ_7745c5c3_Err = absenceComponent(absentDay, false).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(c.ToString()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 148, Col: 26} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(day.ToString()) + 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: 148, Col: 25} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 152, Col: 25} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -493,7 +509,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { }) } -func absentInput(a models.Absence) templ.Component { +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 { @@ -509,64 +525,72 @@ func absentInput(a models.Absence) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var21 := templ.GetChildren(ctx) - if templ_7745c5c3_Var21 == nil { - templ_7745c5c3_Var21 = templ.NopComponent + templ_7745c5c3_Var22 := templ.GetChildren(ctx) + if templ_7745c5c3_Var22 == nil { + templ_7745c5c3_Var22 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + 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 = newBookingComponent(workDay).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(a.DateFrom.Format(time.DateOnly)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 160, Col: 79} + 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_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err + 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 + } + }() } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(a.AbwesenheitTyp.Id) + templ_7745c5c3_Var24, 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: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 177, Col: 18} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\"> ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } 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/DocumentCreator/Dockerfile b/DocumentCreator/Dockerfile index 1989fed..e132f75 100644 --- a/DocumentCreator/Dockerfile +++ b/DocumentCreator/Dockerfile @@ -1,6 +1,7 @@ FROM ghcr.io/typst/typst:0.14.0 -COPY ./templates ./templates -COPY ./static ./static +WORKDIR /app +COPY ./templates /app/templates +COPY ./static /app/static ENTRYPOINT ["sh", "-c", "while true; do sleep 3600; done"] 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: {} -- 2.49.1 From b7de3ade65ad27ddb446eae47bc5f37d13a363c3 Mon Sep 17 00:00:00 2001 From: tom_trgr Date: Wed, 24 Dec 2025 23:20:57 +0100 Subject: [PATCH 31/35] fixed #65, #67 --- Backend/Makefile | 4 +- Backend/endpoints/time.go | 27 +- Backend/models/absence.go | 9 + Backend/static/css/styles.css | 1278 +-------------------- Backend/templates/reportPage.templ | 124 +- Backend/templates/reportPage_templ.go | 477 +++++++- Backend/templates/teamComponents.templ | 120 +- Backend/templates/teamComponents_templ.go | 532 +-------- Backend/templates/timeComponents.templ | 4 +- Backend/templates/timeComponents_templ.go | 4 +- Backend/templates/timePage.templ | 44 +- Backend/templates/timePage_templ.go | 165 ++- DB/initdb/03_create_user.sh | 4 + 13 files changed, 777 insertions(+), 2015 deletions(-) diff --git a/Backend/Makefile b/Backend/Makefile index 53655ef..08eeb44 100644 --- a/Backend/Makefile +++ b/Backend/Makefile @@ -12,5 +12,5 @@ live: live/templ: templ generate --watch --proxy="http://localhost:8080" --cmd="go run ." --open-browser=false -live/tailwind: - npx --yes tailwindcss -i ./input.css -o ./assets/styles.css --minify --watch +live/tailwindcss: + npx --yes tailwindcss -i ./src/main.css -o ./static/css/styles.css --minify --watch diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index 15a6375..1f7985e 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -37,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 } @@ -104,6 +114,21 @@ 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) diff --git a/Backend/models/absence.go b/Backend/models/absence.go index 8b7e789..0f1f03b 100644 --- a/Backend/models/absence.go +++ b/Backend/models/absence.go @@ -294,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/static/css/styles.css b/Backend/static/css/styles.css index ee0ad93..c5dc9bd 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -1,1278 +1,2 @@ /*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */ -@layer properties; -@layer theme, base, components, utilities; -@layer theme { - :root, :host { - --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", - "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", - "Courier New", monospace; - --color-red-500: oklch(63.7% 0.237 25.331); - --color-red-600: oklch(57.7% 0.245 27.325); - --color-red-700: oklch(50.5% 0.213 27.518); - --color-orange-500: oklch(70.5% 0.213 47.604); - --color-purple-600: oklch(55.8% 0.288 302.321); - --color-slate-300: oklch(86.9% 0.022 252.894); - --color-slate-600: oklch(44.6% 0.043 257.281); - --color-slate-700: oklch(37.2% 0.044 257.287); - --color-slate-800: oklch(27.9% 0.041 260.031); - --color-neutral-100: oklch(97% 0 0); - --color-neutral-200: oklch(92.2% 0 0); - --color-neutral-300: oklch(87% 0 0); - --color-neutral-400: oklch(70.8% 0 0); - --color-neutral-500: oklch(55.6% 0 0); - --color-neutral-600: oklch(43.9% 0 0); - --color-neutral-700: oklch(37.1% 0 0); - --color-neutral-800: oklch(26.9% 0 0); - --color-black: #000; - --color-white: #fff; - --spacing: 0.25rem; - --text-sm: 0.875rem; - --text-sm--line-height: calc(1.25 / 0.875); - --text-xl: 1.25rem; - --text-xl--line-height: calc(1.75 / 1.25); - --text-2xl: 1.5rem; - --text-2xl--line-height: calc(2 / 1.5); - --font-weight-bold: 700; - --radius-md: 0.375rem; - --default-transition-duration: 150ms; - --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - --default-font-family: var(--font-sans); - --default-mono-font-family: var(--font-mono); - --color-accent: #0eaf23; - } -} -@layer base { - *, ::after, ::before, ::backdrop, ::file-selector-button { - box-sizing: border-box; - margin: 0; - padding: 0; - border: 0 solid; - } - html, :host { - line-height: 1.5; - -webkit-text-size-adjust: 100%; - tab-size: 4; - font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); - font-feature-settings: var(--default-font-feature-settings, normal); - font-variation-settings: var(--default-font-variation-settings, normal); - -webkit-tap-highlight-color: transparent; - } - hr { - height: 0; - color: inherit; - border-top-width: 1px; - } - abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; - } - h1, h2, h3, h4, h5, h6 { - font-size: inherit; - font-weight: inherit; - } - a { - color: inherit; - -webkit-text-decoration: inherit; - text-decoration: inherit; - } - b, strong { - font-weight: bolder; - } - code, kbd, samp, pre { - font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); - font-feature-settings: var(--default-mono-font-feature-settings, normal); - font-variation-settings: var(--default-mono-font-variation-settings, normal); - font-size: 1em; - } - small { - font-size: 80%; - } - sub, sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; - } - sub { - bottom: -0.25em; - } - sup { - top: -0.5em; - } - table { - text-indent: 0; - border-color: inherit; - border-collapse: collapse; - } - :-moz-focusring { - outline: auto; - } - progress { - vertical-align: baseline; - } - summary { - display: list-item; - } - ol, ul, menu { - list-style: none; - } - img, svg, video, canvas, audio, iframe, embed, object { - display: block; - vertical-align: middle; - } - img, video { - max-width: 100%; - height: auto; - } - button, input, select, optgroup, textarea, ::file-selector-button { - font: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - letter-spacing: inherit; - color: inherit; - border-radius: 0; - background-color: transparent; - opacity: 1; - } - :where(select:is([multiple], [size])) optgroup { - font-weight: bolder; - } - :where(select:is([multiple], [size])) optgroup option { - padding-inline-start: 20px; - } - ::file-selector-button { - margin-inline-end: 4px; - } - ::placeholder { - opacity: 1; - } - @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { - ::placeholder { - color: currentcolor; - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, currentcolor 50%, transparent); - } - } - } - textarea { - resize: vertical; - } - ::-webkit-search-decoration { - -webkit-appearance: none; - } - ::-webkit-date-and-time-value { - min-height: 1lh; - text-align: inherit; - } - ::-webkit-datetime-edit { - display: inline-flex; - } - ::-webkit-datetime-edit-fields-wrapper { - padding: 0; - } - ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { - padding-block: 0; - } - ::-webkit-calendar-picker-indicator { - line-height: 1; - } - :-moz-ui-invalid { - box-shadow: none; - } - button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { - appearance: button; - } - ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { - height: auto; - } - [hidden]:where(:not([hidden="until-found"])) { - display: none !important; - } -} -@layer utilities { - .\@container { - container-type: inline-size; - } - .absolute { - position: absolute; - } - .relative { - position: relative; - } - .top-1 { - top: calc(var(--spacing) * 1); - } - .top-1\/2 { - top: calc(1/2 * 100%); - } - .top-2 { - top: calc(var(--spacing) * 2); - } - .top-2\.5 { - top: calc(var(--spacing) * 2.5); - } - .top-\[0\.125rem\] { - top: 0.125rem; - } - .right-1 { - right: calc(var(--spacing) * 1); - } - .right-2 { - right: calc(var(--spacing) * 2); - } - .right-2\.5 { - right: calc(var(--spacing) * 2.5); - } - .left-1 { - left: calc(var(--spacing) * 1); - } - .left-1\/2 { - left: calc(1/2 * 100%); - } - .col-span-2 { - grid-column: span 2 / span 2; - } - .col-span-3 { - grid-column: span 3 / span 3; - } - .col-span-full { - grid-column: 1 / -1; - } - .mx-auto { - margin-inline: auto; - } - .-my-1 { - margin-block: calc(var(--spacing) * -1); - } - .mt-1 { - margin-top: calc(var(--spacing) * 1); - } - .mb-1 { - margin-bottom: calc(var(--spacing) * 1); - } - .mb-2 { - margin-bottom: calc(var(--spacing) * 2); - } - .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; - height: 1.25em; - background-color: currentColor; - -webkit-mask-image: var(--svg); - mask-image: var(--svg); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: 100% 100%; - mask-size: 100% 100%; - --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m8.4 16.308l3.6-3.6l3.6 3.6l.708-.708l-3.6-3.6l3.6-3.6l-.708-.708l-3.6 3.6l-3.6-3.6l-.708.708l3.6 3.6l-3.6 3.6zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E"); - } - .icon-\[material-symbols-light--check-circle-outline\] { - display: inline-block; - width: 1.25em; - height: 1.25em; - background-color: currentColor; - -webkit-mask-image: var(--svg); - mask-image: var(--svg); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: 100% 100%; - mask-size: 100% 100%; - --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m10.562 15.908l6.396-6.396l-.708-.708l-5.688 5.688l-2.85-2.85l-.708.708zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E"); - } - .icon-\[material-symbols-light--circle-outline\] { - display: inline-block; - width: 1.25em; - height: 1.25em; - background-color: currentColor; - -webkit-mask-image: var(--svg); - mask-image: var(--svg); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: 100% 100%; - mask-size: 100% 100%; - --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E"); - } - .icon-\[material-symbols-light--delete-outline\] { - display: inline-block; - width: 1.25em; - height: 1.25em; - background-color: currentColor; - -webkit-mask-image: var(--svg); - mask-image: var(--svg); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: 100% 100%; - mask-size: 100% 100%; - --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M7.616 20q-.672 0-1.144-.472T6 18.385V6H5V5h4v-.77h6V5h4v1h-1v12.385q0 .69-.462 1.153T16.384 20zM17 6H7v12.385q0 .269.173.442t.443.173h8.769q.23 0 .423-.192t.192-.424zM9.808 17h1V8h-1zm3.384 0h1V8h-1zM7 6v13z'/%3E%3C/svg%3E"); - } - .icon-\[material-symbols-light--more-time\] { - display: inline-block; - width: 1.25em; - height: 1.25em; - background-color: currentColor; - -webkit-mask-image: var(--svg); - mask-image: var(--svg); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: 100% 100%; - mask-size: 100% 100%; - --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M11.003 20q-1.666 0-3.123-.622t-2.545-1.71t-1.712-2.544T3 12.003t.622-3.123t1.711-2.546q1.09-1.089 2.545-1.711T11 4q.525 0 1.013.063T13 4.25V5.3q-.5-.15-.987-.225T11 5Q8.089 5 6.044 7.044T4 12t2.044 4.956T11 19t4.956-2.044T18 11.996q0-.271-.025-.554t-.094-.557h1.011q.05.236.08.538q.028.302.028.577q0 1.667-.622 3.122t-1.71 2.545q-1.089 1.088-2.544 1.71q-1.455.623-3.121.623m3.143-4.146L10.5 12.208V7h1v4.792l3.354 3.354zM18 8.884v-3h-3v-1h3v-3h1v3h3v1h-3v3z'/%3E%3C/svg%3E"); - } - .icon-\[material-symbols-light--motion-photos-paused-outline\] { - display: inline-block; - width: 1.25em; - height: 1.25em; - background-color: currentColor; - -webkit-mask-image: var(--svg); - mask-image: var(--svg); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: 100% 100%; - mask-size: 100% 100%; - --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9.808 14.616h1V9.385h-1zm3.384 0h1V9.385h-1zM12.003 21q-1.866 0-3.51-.705q-1.643-.706-2.859-1.915t-1.925-2.843T3 12.039q0-.905.167-1.778t.497-1.713l.78.78q-.219.65-.331 1.32T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.675 0-1.332.112t-1.3.332l-.776-.775q.789-.315 1.606-.492T11.885 3q1.887 0 3.546.701t2.894 1.926t1.955 2.866t.72 3.505t-.708 3.509t-1.924 2.859t-2.856 1.925t-3.509.709M5.923 6.808q-.356 0-.62-.265q-.264-.264-.264-.62t.264-.62t.62-.264t.62.264t.265.62t-.265.62t-.62.265M12 12'/%3E%3C/svg%3E"); - } - .icon-\[material-symbols-light--schedule-outline\] { - display: inline-block; - width: 1.25em; - height: 1.25em; - background-color: currentColor; - -webkit-mask-image: var(--svg); - mask-image: var(--svg); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: 100% 100%; - mask-size: 100% 100%; - --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m15.646 16.354l.708-.708l-3.854-3.854V7h-1v5.208zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.325 0 5.663-2.337T20 12t-2.337-5.663T12 4T6.337 6.338T4 12t2.338 5.663T12 20'/%3E%3C/svg%3E"); - } - .block { - display: block; - } - .flex { - display: flex; - } - .grid { - display: grid; - } - .hidden { - display: none; - } - .inline { - display: inline; - } - .inline-flex { - display: inline-flex; - } - .table { - display: table; - } - .size-2 { - width: calc(var(--spacing) * 2); - height: calc(var(--spacing) * 2); - } - .size-4 { - width: calc(var(--spacing) * 4); - height: calc(var(--spacing) * 4); - } - .size-5 { - width: calc(var(--spacing) * 5); - height: calc(var(--spacing) * 5); - } - .h-2 { - height: calc(var(--spacing) * 2); - } - .h-3 { - height: calc(var(--spacing) * 3); - } - .h-3\.5 { - height: calc(var(--spacing) * 3.5); - } - .h-4 { - height: calc(var(--spacing) * 4); - } - .h-5 { - height: calc(var(--spacing) * 5); - } - .h-8 { - height: calc(var(--spacing) * 8); - } - .h-\[100vh\] { - height: 100vh; - } - .h-full { - height: 100%; - } - .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%); - } - .w-\[2px\] { - width: 2px; - } - .w-auto { - width: auto; - } - .w-full { - width: 100%; - } - .flex-shrink { - flex-shrink: 1; - } - .flex-shrink-0 { - flex-shrink: 0; - } - .flex-grow { - flex-grow: 1; - } - .grow-0 { - flex-grow: 0; - } - .grow-1 { - flex-grow: 1; - } - .basis-\[content\] { - flex-basis: content; - } - .border-collapse { - border-collapse: collapse; - } - .-translate-x-1 { - --tw-translate-x: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - .-translate-x-1\/2 { - --tw-translate-x: calc(calc(1/2 * 100%) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - .-translate-y-1 { - --tw-translate-y: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - .-translate-y-1\/2 { - --tw-translate-y: calc(calc(1/2 * 100%) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - .transform { - transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); - } - .cursor-pointer { - cursor: pointer; - } - .resize { - resize: both; - } - .scroll-m-2 { - scroll-margin: calc(var(--spacing) * 2); - } - .appearance-none { - appearance: none; - } - .break-after-page { - break-after: page; - } - .auto-rows-min { - grid-auto-rows: min-content; - } - .grid-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .grid-cols-5 { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - .grid-cols-\[3fr_2fr_2fr_2fr_3fr_3fr_3fr\] { - grid-template-columns: 3fr 2fr 2fr 2fr 3fr 3fr 3fr; - } - .grid-cols-subgrid { - grid-template-columns: subgrid; - } - .grid-rows-6 { - grid-template-rows: repeat(6, minmax(0, 1fr)); - } - .flex-col { - flex-direction: column; - } - .flex-row { - flex-direction: row; - } - .content-baseline { - align-content: baseline; - } - .content-end { - align-content: flex-end; - } - .items-center { - align-items: center; - } - .items-end { - align-items: flex-end; - } - .justify-around { - justify-content: space-around; - } - .justify-between { - justify-content: space-between; - } - .justify-center { - justify-content: center; - } - .gap-2 { - gap: calc(var(--spacing) * 2); - } - .gap-4 { - gap: calc(var(--spacing) * 4); - } - .divide-x-1 { - :where(& > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); - } - } - .divide-y-1 { - :where(& > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(1px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); - } - } - .divide-neutral-300 { - :where(& > :not(:last-child)) { - border-color: var(--color-neutral-300); - } - } - .justify-self-end { - justify-self: flex-end; - } - .overflow-hidden { - overflow: hidden; - } - .rounded { - border-radius: 0.25rem; - } - .rounded-full { - border-radius: calc(infinity * 1px); - } - .rounded-md { - border-radius: var(--radius-md); - } - .rounded-none { - border-radius: 0; - } - .border { - border-style: var(--tw-border-style); - border-width: 1px; - } - .border-0 { - border-style: var(--tw-border-style); - border-width: 0px; - } - .border-r-0 { - border-right-style: var(--tw-border-style); - border-right-width: 0px; - } - .border-r-1 { - border-right-style: var(--tw-border-style); - border-right-width: 1px; - } - .border-b-0 { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 0px; - } - .border-dashed { - --tw-border-style: dashed; - border-style: dashed; - } - .border-neutral-300 { - border-color: var(--color-neutral-300); - } - .border-neutral-500 { - border-color: var(--color-neutral-500); - } - .border-neutral-600 { - border-color: var(--color-neutral-600); - } - .border-slate-300 { - border-color: var(--color-slate-300); - } - .border-slate-700 { - border-color: var(--color-slate-700); - } - .border-slate-800 { - border-color: var(--color-slate-800); - } - .bg-accent { - background-color: var(--color-accent); - } - .bg-neutral-100 { - background-color: var(--color-neutral-100); - } - .bg-neutral-300 { - background-color: var(--color-neutral-300); - } - .bg-neutral-400 { - background-color: var(--color-neutral-400); - } - .bg-orange-500 { - background-color: var(--color-orange-500); - } - .bg-purple-600 { - background-color: var(--color-purple-600); - } - .bg-red-600 { - background-color: var(--color-red-600); - } - .mask-repeat { - mask-repeat: repeat; - } - .p-1 { - padding: calc(var(--spacing) * 1); - } - .p-2 { - padding: calc(var(--spacing) * 2); - } - .p-8 { - padding: calc(var(--spacing) * 8); - } - .px-3 { - padding-inline: calc(var(--spacing) * 3); - } - .py-2 { - padding-block: calc(var(--spacing) * 2); - } - .py-4 { - padding-block: calc(var(--spacing) * 4); - } - .text-center { - text-align: center; - } - .text-2xl { - font-size: var(--text-2xl); - line-height: var(--tw-leading, var(--text-2xl--line-height)); - } - .text-sm { - font-size: var(--text-sm); - line-height: var(--tw-leading, var(--text-sm--line-height)); - } - .text-xl { - font-size: var(--text-xl); - line-height: var(--tw-leading, var(--text-xl--line-height)); - } - .font-bold { - --tw-font-weight: var(--font-weight-bold); - font-weight: var(--font-weight-bold); - } - .whitespace-nowrap { - white-space: nowrap; - } - .text-accent { - color: var(--color-accent); - } - .text-black { - color: var(--color-black); - } - .text-neutral-300 { - color: var(--color-neutral-300); - } - .text-neutral-500 { - color: var(--color-neutral-500); - } - .text-neutral-700 { - color: var(--color-neutral-700); - } - .text-neutral-800 { - color: var(--color-neutral-800); - } - .text-red-500 { - color: var(--color-red-500); - } - .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,); - } - .transition { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .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)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .duration-300 { - --tw-duration: 300ms; - transition-duration: 300ms; - } - .select-none { - -webkit-user-select: none; - user-select: none; - } - .\*\:text-center { - :is(& > *) { - text-align: center; - } - } - .\*\:not-print\:p-2 { - :is(& > *) { - @media not print { - padding: calc(var(--spacing) * 2); - } - } - } - .group-hover\:text-black { - &:is(:where(.group):hover *) { - @media (hover: hover) { - color: var(--color-black); - } - } - } - .group-hover\:text-white { - &:is(:where(.group):hover *) { - @media (hover: hover) { - color: var(--color-white); - } - } - } - .group-\[\.edit\]\:ml-2 { - &:is(:where(.group):is(.edit) *) { - margin-left: calc(var(--spacing) * 2); - } - } - .group-\[\.edit\]\:flex { - &:is(:where(.group):is(.edit) *) { - display: flex; - } - } - .group-\[\.edit\]\:hidden { - &:is(:where(.group):is(.edit) *) { - display: none; - } - } - .group-\[\.edit\]\:inline { - &:is(:where(.group):is(.edit) *) { - display: inline; - } - } - .group-\[\.edit\]\/button\:block { - &:is(:where(.group\/button):is(.edit) *) { - display: block; - } - } - .group-\[\.edit\]\/button\:hidden { - &:is(:where(.group\/button):is(.edit) *) { - display: none; - } - } - .peer-checked\:opacity-100 { - &:is(:where(.peer):checked ~ *) { - opacity: 100%; - } - } - .placeholder\:text-neutral-400 { - &::placeholder { - color: var(--color-neutral-400); - } - } - .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) { - border-color: var(--color-neutral-500); - } - } - } - .hover\:bg-neutral-200 { - &:hover { - @media (hover: hover) { - background-color: var(--color-neutral-200); - } - } - } - .hover\:bg-neutral-700 { - &:hover { - @media (hover: hover) { - background-color: var(--color-neutral-700); - } - } - } - .hover\:bg-red-700 { - &:hover { - @media (hover: hover) { - background-color: var(--color-red-700); - } - } - } - .hover\:text-white { - &:hover { - @media (hover: hover) { - color: var(--color-white); - } - } - } - .focus\:bg-neutral-700 { - &:focus { - background-color: var(--color-neutral-700); - } - } - .focus\:outline-none { - &:focus { - --tw-outline-style: none; - outline-style: none; - } - } - .active\:bg-neutral-700 { - &:active { - background-color: var(--color-neutral-700); - } - } - .disabled\:pointer-events-none { - &:disabled { - pointer-events: none; - } - } - .disabled\:opacity-50 { - &:disabled { - opacity: 50%; - } - } - .max-md\:grid { - @media (width < 48rem) { - display: grid; - } - } - .max-md\:hidden { - @media (width < 48rem) { - display: none; - } - } - .max-md\:divide-y-1 { - @media (width < 48rem) { - :where(& > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(1px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); - } - } - } - .max-md\:bg-neutral-300 { - @media (width < 48rem) { - background-color: var(--color-neutral-300); - } - } - .md\:col-span-1 { - @media (width >= 48rem) { - grid-column: span 1 / span 1; - } - } - .md\:col-span-3 { - @media (width >= 48rem) { - grid-column: span 3 / span 3; - } - } - .md\:mx-\[10\%\] { - @media (width >= 48rem) { - margin-inline: 10%; - } - } - .md\:block { - @media (width >= 48rem) { - display: block; - } - } - .md\:hidden { - @media (width >= 48rem) { - display: none; - } - } - .md\:inline { - @media (width >= 48rem) { - display: inline; - } - } - .md\:w-1\/2 { - @media (width >= 48rem) { - 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); - } - } - .md\:text-transparent { - @media (width >= 48rem) { - color: transparent; - } - } - .group-\[\.edit\]\/button\:md\:block { - &:is(:where(.group\/button):is(.edit) *) { - @media (width >= 48rem) { - display: block; - } - } - } - .lg\:hidden { - @media (width >= 64rem) { - display: none; - } - } - .lg\:grid-cols-1 { - @media (width >= 64rem) { - grid-template-columns: repeat(1, minmax(0, 1fr)); - } - } - .lg\:divide-x-1 { - @media (width >= 64rem) { - :where(& > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); - } - } - } - .lg\:border-0 { - @media (width >= 64rem) { - border-style: var(--tw-border-style); - border-width: 0px; - } - } - .\@7xl\:grid { - @container (width >= 80rem) { - display: grid; - } - } - .\@7xl\:grid-cols-5 { - @container (width >= 80rem) { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - } - .print\:hidden { - @media print { - display: none; - } - } -} -@layer base { - body { - -webkit-print-color-adjust: exact !important; - print-color-adjust: exact !important; - background-color: white; - } -} -@layer components { - .grid-main { - display: grid; - grid-template-columns: 4fr 3fr 3fr 1fr; - align-items: stretch; - } - .grid-sub { - display: grid; - grid-template-columns: subgrid; - grid-column: 1 / -1; - border-color: var(--color-neutral-400); - transition: background-color 0.2s ease-in-out; - } - .grid-sub.responsive { - display: flex; - flex-direction: column; - } - .grid-sub:hover { - background-color: var(--color-neutral-200); - } - .grid-cell { - padding: calc(var(--spacing) * 2); - border-color: var(--color-neutral-400); - } - .btn { - width: 100%; - cursor: pointer; - border-radius: var(--radius-md); - color: var(--color-neutral-800); - font-size: var(--text-sm); - text-align: center; - padding: calc(var(--spacing) * 2); - border-style: var(--tw-border-style); - border-width: 1px; - border-color: var(--color-neutral-800); - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; - transition-timing-function: var( --tw-ease, var(--default-transition-timing-function) ); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - input.btn, select.btn { - transition-duration: 300ms; - } - .btn:hover { - color: var(--color-white); - background-color: var(--color-neutral-700); - } - .btn:disabled { - opacity: 50%; - pointer-events: none; - } - input.btn, select.btn { - text-align: left; - } - input.btn:hover, select.btn:hover { - border-color: var(--color-neutral-300); - background-color: var(--color-neutral-100); - color: var(--color-neutral-800); - } - .edit-box { - border-radius: var(--radius-md); - overflow: hidden; - border-color: var(--color-neutral-500); - transition-property: background-color, border-color; - transition-timing-function: var(--default-transition-timing-function) * 2; - transition-duration: var(--default-transition-duration); - outline: none; - &:is(:where(.group):is(.edit) *) { - border-width: 1px; - } - } - .edit-box:hover { - &:is(:where(.group):is(.edit) *) { - background-color: var(--color-white); - border-color: var(--color-neutral-300); - } - } - .edit-box input:focus { - outline: none; - } - div.edit { - border-width: 1px; - background-color: var(--color-neutral-300); - } - @media (width >=48rem) { - .grid-main { - grid-template-columns: repeat(5, 1fr); - margin: 0 10%; - } - .grid-sub.responsive { - display: grid; - } - .btn { - padding-inline: calc(var(--spacing) * 4); - } - } -} -@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; - initial-value: 0; -} -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -@property --tw-divide-y-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-font-weight { - syntax: "*"; - inherits: false; -} -@property --tw-outline-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -@property --tw-blur { - syntax: "*"; - inherits: false; -} -@property --tw-brightness { - syntax: "*"; - inherits: false; -} -@property --tw-contrast { - syntax: "*"; - inherits: false; -} -@property --tw-grayscale { - syntax: "*"; - inherits: false; -} -@property --tw-hue-rotate { - syntax: "*"; - inherits: false; -} -@property --tw-invert { - syntax: "*"; - inherits: false; -} -@property --tw-opacity { - syntax: "*"; - inherits: false; -} -@property --tw-saturate { - syntax: "*"; - inherits: false; -} -@property --tw-sepia { - syntax: "*"; - inherits: false; -} -@property --tw-drop-shadow { - syntax: "*"; - inherits: false; -} -@property --tw-drop-shadow-color { - syntax: "*"; - inherits: false; -} -@property --tw-drop-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-drop-shadow-size { - syntax: "*"; - inherits: false; -} -@property --tw-duration { - syntax: "*"; - inherits: false; -} -@layer properties { - @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { - *, ::before, ::after, ::backdrop { - --tw-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; - --tw-grayscale: initial; - --tw-hue-rotate: initial; - --tw-invert: initial; - --tw-opacity: initial; - --tw-saturate: initial; - --tw-sepia: initial; - --tw-drop-shadow: initial; - --tw-drop-shadow-color: initial; - --tw-drop-shadow-alpha: 100%; - --tw-drop-shadow-size: initial; - --tw-duration: initial; - } - } -} +@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-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-orange-500:oklch(70.5% .213 47.604);--color-purple-600:oklch(55.8% .288 302.321);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-bold:700;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-accent:#0eaf23}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}body{background-color:#fff;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}}@layer components{.grid-main{grid-template-columns:4fr 3fr 3fr 1fr;align-items:stretch;display:grid}.grid-sub{grid-template-columns:subgrid;border-color:var(--color-neutral-400);grid-column:1/-1;transition:background-color .2s ease-in-out;display:grid}.grid-sub.responsive{flex-direction:column;display:flex}.grid-sub:hover{background-color:var(--color-neutral-200)}.grid-cell{padding:calc(var(--spacing)*2);border-color:var(--color-neutral-400)}.btn{cursor:pointer;border-radius:var(--radius-md);width:100%;color:var(--color-neutral-800);font-size:var(--text-sm);text-align:center;padding:calc(var(--spacing)*2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-neutral-800);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}input.btn,select.btn{transition-duration:.3s}.btn:hover{color:var(--color-white);background-color:var(--color-neutral-700)}.btn:disabled{opacity:.5;pointer-events:none}input.btn,select.btn{text-align:left}input.btn:hover,select.btn:hover{border-color:var(--color-neutral-300);background-color:var(--color-neutral-100);color:var(--color-neutral-800)}.edit-box{border-radius:var(--radius-md);border-color:var(--color-neutral-500);transition-property:background-color,border-color;transition-timing-function:var(--default-transition-timing-function)*2;transition-duration:var(--default-transition-duration);outline:none;overflow:hidden}.edit-box:is(:where(.group).edit *){border-width:1px}.edit-box:hover:is(:where(.group).edit *){background-color:var(--color-white);border-color:var(--color-neutral-300)}.edit-box input:focus{outline:none}div.edit{background-color:var(--color-neutral-300);border-width:1px}@media (min-width:48rem){.grid-main{grid-template-columns:repeat(5,1fr);margin:0 10%}.grid-sub.responsive{display:grid}.btn{padding-inline:calc(var(--spacing)*4)}}}@layer utilities{.\@container{container-type:inline-size}.absolute{position:absolute}.relative{position:relative}.top-1\/2{top:50%}.top-2\.5{top:calc(var(--spacing)*2.5)}.top-\[0\.125rem\]{top:.125rem}.right-1{right:calc(var(--spacing)*1)}.right-2\.5{right:calc(var(--spacing)*2.5)}.left-1\/2{left:50%}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-full{grid-column:1/-1}.mx-auto{margin-inline:auto}.-my-1{margin-block:calc(var(--spacing)*-1)}.mt-1{margin-top:calc(var(--spacing)*1)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.icon-\[material-symbols-light--cancel-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m8.4 16.308l3.6-3.6l3.6 3.6l.708-.708l-3.6-3.6l3.6-3.6l-.708-.708l-3.6 3.6l-3.6-3.6l-.708.708l3.6 3.6l-3.6 3.6zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--check-circle-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m10.562 15.908l6.396-6.396l-.708-.708l-5.688 5.688l-2.85-2.85l-.708.708zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--circle-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--delete-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M7.616 20q-.672 0-1.144-.472T6 18.385V6H5V5h4v-.77h6V5h4v1h-1v12.385q0 .69-.462 1.153T16.384 20zM17 6H7v12.385q0 .269.173.442t.443.173h8.769q.23 0 .423-.192t.192-.424zM9.808 17h1V8h-1zm3.384 0h1V8h-1zM7 6v13z'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--more-time\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M11.003 20q-1.666 0-3.123-.622t-2.545-1.71t-1.712-2.544T3 12.003t.622-3.123t1.711-2.546q1.09-1.089 2.545-1.711T11 4q.525 0 1.013.063T13 4.25V5.3q-.5-.15-.987-.225T11 5Q8.089 5 6.044 7.044T4 12t2.044 4.956T11 19t4.956-2.044T18 11.996q0-.271-.025-.554t-.094-.557h1.011q.05.236.08.538q.028.302.028.577q0 1.667-.622 3.122t-1.71 2.545q-1.089 1.088-2.544 1.71q-1.455.623-3.121.623m3.143-4.146L10.5 12.208V7h1v4.792l3.354 3.354zM18 8.884v-3h-3v-1h3v-3h1v3h3v1h-3v3z'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--motion-photos-paused-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9.808 14.616h1V9.385h-1zm3.384 0h1V9.385h-1zM12.003 21q-1.866 0-3.51-.705q-1.643-.706-2.859-1.915t-1.925-2.843T3 12.039q0-.905.167-1.778t.497-1.713l.78.78q-.219.65-.331 1.32T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.675 0-1.332.112t-1.3.332l-.776-.775q.789-.315 1.606-.492T11.885 3q1.887 0 3.546.701t2.894 1.926t1.955 2.866t.72 3.505t-.708 3.509t-1.924 2.859t-2.856 1.925t-3.509.709M5.923 6.808q-.356 0-.62-.265q-.264-.264-.264-.62t.264-.62t.62-.264t.62.264t.265.62t-.265.62t-.62.265M12 12'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--schedule-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m15.646 16.354l.708-.708l-3.854-3.854V7h-1v5.208zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.325 0 5.663-2.337T20 12t-2.337-5.663T12 4T6.337 6.338T4 12t2.338 5.663T12 20'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-5{width:calc(var(--spacing)*5);height:calc(var(--spacing)*5)}.h-2{height:calc(var(--spacing)*2)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-\[100vh\]{height:100vh}.h-full{height:100%}.w-2{width:calc(var(--spacing)*2)}.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\/10{width:90%}.w-\[2px\]{width:2px}.w-auto{width:auto}.w-full{width:100%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.grow-0{flex-grow:0}.grow-1{flex-grow:1}.basis-\[content\]{flex-basis:content}.-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\/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}.scroll-m-2{scroll-margin:calc(var(--spacing)*2)}.appearance-none{appearance:none}.break-after-page{break-after:page}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-\[3fr_2fr_2fr_2fr_3fr_3fr_3fr\]{grid-template-columns:3fr 2fr 2fr 2fr 3fr 3fr 3fr}.grid-cols-subgrid{grid-template-columns:subgrid}.grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.content-baseline{align-content:baseline}.content-end{align-content:flex-end}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}:where(.divide-x-1>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y-1>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-neutral-300>:not(:last-child)){border-color:var(--color-neutral-300)}.justify-self-end{justify-self:flex-end}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-r-1{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-neutral-300{border-color:var(--color-neutral-300)}.border-neutral-500{border-color:var(--color-neutral-500)}.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)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-300{background-color:var(--color-neutral-300)}.bg-neutral-400{background-color:var(--color-neutral-400)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-purple-600{background-color:var(--color-purple-600)}.bg-red-600{background-color:var(--color-red-600)}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.p-8{padding:calc(var(--spacing)*8)}.px-3{padding-inline:calc(var(--spacing)*3)}.py-2{padding-block:calc(var(--spacing)*2)}.py-4{padding-block:calc(var(--spacing)*4)}.text-center{text-align:center}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.whitespace-nowrap{white-space:nowrap}.text-accent{color:var(--color-accent)}.text-black{color:var(--color-black)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.text-red-500{color:var(--color-red-500)}.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}.opacity-0{opacity:0}.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,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,visibility,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.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));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.select-none{-webkit-user-select:none;user-select:none}:is(.\*\:text-center>*){text-align:center}@media not print{:is(.\*\:not-print\:p-2>*){padding:calc(var(--spacing)*2)}}@media (hover:hover){.group-hover\:text-black:is(:where(.group):hover *){color:var(--color-black)}.group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}}.group-\[\.edit\]\:ml-2:is(:where(.group).edit *){margin-left:calc(var(--spacing)*2)}.group-\[\.edit\]\:flex:is(:where(.group).edit *){display:flex}.group-\[\.edit\]\:hidden:is(:where(.group).edit *){display:none}.group-\[\.edit\]\:inline:is(:where(.group).edit *){display:inline}.group-\[\.edit\]\/button\:block:is(:where(.group\/button).edit *){display:block}.group-\[\.edit\]\/button\:hidden:is(:where(.group\/button).edit *){display:none}.peer-checked\:opacity-100:is(:where(.peer):checked~*){opacity:1}.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)}@media (hover:hover){.hover\:border-neutral-500:hover{border-color:var(--color-neutral-500)}.hover\:bg-neutral-200:hover{background-color:var(--color-neutral-200)}.hover\:bg-neutral-700:hover{background-color:var(--color-neutral-700)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-white:hover{color:var(--color-white)}}.focus\:bg-neutral-700:focus{background-color:var(--color-neutral-700)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:bg-neutral-700:active{background-color:var(--color-neutral-700)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}@media not all and (min-width:48rem){.max-md\:grid{display:grid}.max-md\:hidden{display:none}:where(.max-md\:divide-y-1>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}.max-md\:bg-neutral-300{background-color:var(--color-neutral-300)}}@media (min-width:48rem){.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-3{grid-column:span 3/span 3}.md\:mx-\[10\%\]{margin-inline:10%}.md\:block{display:block}.md\:hidden{display:none}.md\:inline{display:inline}.md\:w-1\/2{width:50%}.md\:flex-row{flex-direction:row}.md\:px-4{padding-inline:calc(var(--spacing)*4)}.md\:text-transparent{color:#0000}.group-\[\.edit\]\/button\:md\:block:is(:where(.group\/button).edit *){display:block}}@media (min-width:64rem){.lg\:hidden{display:none}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}:where(.lg\:divide-x-1>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}.lg\:border-0{border-style:var(--tw-border-style);border-width:0}}@container (min-width:80rem){.\@7xl\:grid{display:grid}.\@7xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}@media print{.print\:hidden{display:none}}}@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;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false} \ No newline at end of file diff --git a/Backend/templates/reportPage.templ b/Backend/templates/reportPage.templ index df2bab2..bc74e34 100644 --- a/Backend/templates/reportPage.templ +++ b/Backend/templates/reportPage.templ @@ -1,6 +1,12 @@ package templates -import "arbeitszeitmessung/models" +import ( + "arbeitszeitmessung/helper" + "arbeitszeitmessung/models" + "fmt" + "strconv" + "time" +) templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) { @Base() @@ -22,3 +28,119 @@ templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) { } } + +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 index 6a97c20..96ed8f5 100644 --- a/Backend/templates/reportPage_templ.go +++ b/Backend/templates/reportPage_templ.go @@ -8,7 +8,13 @@ package templates import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -import "arbeitszeitmessung/models" +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) { @@ -67,4 +73,473 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component }) } +func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + 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 9790ef7..fd09824 100644 --- a/Backend/templates/teamComponents.templ +++ b/Backend/templates/teamComponents.templ @@ -1,10 +1,8 @@ package templates import ( - "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "fmt" - "strconv" "time" ) @@ -26,38 +24,7 @@ templ weekPicker(weekStart time.Time) { } -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) }} -
- { helper.FormatDuration(work) } - { helper.FormatDuration(pause) } -
- switch day.Type() { - case models.DayTypeWorkday: - {{ workDay, _ := day.(*models.WorkDay) }} - @workDayWeekComponent(workDay, u) - case models.DayTypeCompound: - for _, c := range day.(*models.CompoundDay).DayParts { - switch c.Type() { - case models.DayTypeWorkday: - {{ workDay, _ := c.(*models.WorkDay) }} - @workDayWeekComponent(workDay, u) - default: -
{ c.ToString() }
- } - } - default: -
{ day.ToString() }
- } -
-
-} - -templ workDayWeekComponent(workDay *models.WorkDay, u models.User) { +templ workDayWeekComponent(workDay *models.WorkDay) { if !workDay.RequiresAction() {
@@ -75,91 +42,6 @@ templ workDayWeekComponent(workDay *models.WorkDay, u models.User) { } } -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 userPresenceComponent(user models.User, present bool) {
if present { diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index e65c224..0078d43 100644 --- a/Backend/templates/teamComponents_templ.go +++ b/Backend/templates/teamComponents_templ.go @@ -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" ) @@ -45,7 +43,7 @@ func weekPicker(weekStart time.Time) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(weekStart.Format(time.DateOnly)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 14, Col: 98} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 12, Col: 98} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -75,7 +73,7 @@ func weekPicker(weekStart time.Time) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d, %d", kw, year)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 20, Col: 69} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 18, Col: 69} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -116,7 +114,7 @@ func weekPicker(weekStart time.Time) templ.Component { }) } -func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component { +func workDayWeekComponent(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 { @@ -137,208 +135,55 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component templ_7745c5c3_Var6 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") - 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: 33, Col: 152} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - work, pause, _ := day.GetTimes(u, models.WorktimeBaseDay, false) - 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: 36, Col: 59} - } - _, 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: 37, Col: 65} - } - _, 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 day.Type() { - case models.DayTypeWorkday: - workDay, _ := day.(*models.WorkDay) - templ_7745c5c3_Err = workDayWeekComponent(workDay, u).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - case models.DayTypeCompound: - for _, c := range day.(*models.CompoundDay).DayParts { - switch c.Type() { - case models.DayTypeWorkday: - workDay, _ := c.(*models.WorkDay) - templ_7745c5c3_Err = workDayWeekComponent(workDay, u).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - default: - 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(c.ToString()) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 50, Col: 27} - } - _, 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 - } - } - } - default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(day.ToString()) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 54, Col: 26} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func workDayWeekComponent(workDay *models.WorkDay, u models.User) templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var13 := templ.GetChildren(ctx) - if templ_7745c5c3_Var13 == nil { - templ_7745c5c3_Var13 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) if !workDay.RequiresAction() { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } switch { case !workDay.TimeFrom.Equal(workDay.TimeTo): - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.TimeFrom.Format("15:04")) + 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: 66, Col: 45} + 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_Var14)) + _, 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, 24, " - ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " - ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(workDay.TimeTo.Format("15:04")) + 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: 68, Col: 43} + 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_Var15)) + _, 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, 25, "") + 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, 26, "

Keine Anwesenheit

") + 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, 27, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

Bitte anpassen

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

Bitte anpassen

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -347,313 +192,6 @@ func workDayWeekComponent(workDay *models.WorkDay, u models.User) templ.Componen }) } -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_Var16 := templ.GetChildren(ctx) - if templ_7745c5c3_Var16 == nil { - templ_7745c5c3_Var16 = 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, 29, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !onlyAccept { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
") - 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, 31, "
") - 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.Vorname) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 90, Col: 53} - } - _, 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 - } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, 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: 90, Col: 72} - } - _, 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, 34, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !onlyAccept { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") - 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, 36, "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, 37, "Akzeptiert
") - 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 - } - 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, 39, "

Arbeitszeit: ") - 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.FormatDuration(week.Worktime))) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 107, Col: 79} - } - _, 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, "

Überstunden: ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, 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: 108, Col: 90} - } - _, 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, 41, "

") - 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, 42, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if onlyAccept { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

Woche: ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, 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: 120, Col: 86} - } - _, 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, 44, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") - 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, 46, "
") - 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 - } - week.CheckStatus() - method := "accept" - if !onlyAccept { - method = "send" - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if onlyAccept { - if week.Status == models.WeekStatusDifferences { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "

Unterschiedliche Arbeitszeit zwischen Abrechnung und individuellen Buchungen

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

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

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

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

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

vom Vorgesetzten bestätigt

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " Korrigieren = models.WeekStatusSent || week.RequiresAction() { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " disabled") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, " type=\"submit\" class=\"btn\">Senden") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - func userPresenceComponent(user models.User, present 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 @@ -670,53 +208,53 @@ func userPresenceComponent(user models.User, present bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var25 := templ.GetChildren(ctx) - if templ_7745c5c3_Var25 == nil { - templ_7745c5c3_Var25 = 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, 67, "
") + 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, 68, "
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, 69, "
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, 70, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, 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: 170, 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_Var26)) + _, 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, 71, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, 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: 170, 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_Var27)) + _, 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, 72, "

") + 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 e2f0d62..8020919 100644 --- a/Backend/templates/timeComponents.templ +++ b/Backend/templates/timeComponents.templ @@ -20,7 +20,7 @@ templ lineComponent() { } templ changeButtonComponent(id string, workDay bool) { - - + } templ timeGaugeComponent(progress int8, today bool) { diff --git a/Backend/templates/timeComponents_templ.go b/Backend/templates/timeComponents_templ.go index ea98718..2e42a6f 100644 --- a/Backend/templates/timeComponents_templ.go +++ b/Backend/templates/timeComponents_templ.go @@ -69,7 +69,7 @@ func changeButtonComponent(id string, workDay bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
@@ -160,6 +137,23 @@ templ defaultDayComponent(day models.IWorkDay) {
} +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!

diff --git a/Backend/templates/timePage_templ.go b/Backend/templates/timePage_templ.go index 3484084..7755a83 100644 --- a/Backend/templates/timePage_templ.go +++ b/Backend/templates/timePage_templ.go @@ -410,90 +410,15 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - switch day.Type() { - case models.DayTypeWorkday: - workDay, _ := day.(*models.WorkDay) - templ_7745c5c3_Err = workdayComponent(workDay).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = newAbsenceComponent().Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - case models.DayTypeAbsence: - absentDay, _ := day.(*models.Absence) - templ_7745c5c3_Err = absenceComponent(absentDay, false).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - case models.DayTypeCompound: - for _, c := range day.(*models.CompoundDay).DayParts { - switch c.Type() { - case models.DayTypeWorkday: - workDay, _ := c.(*models.WorkDay) - templ_7745c5c3_Err = workdayComponent(workDay).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = newAbsenceComponent().Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - case models.DayTypeAbsence: - absentDay, _ := c.(*models.Absence) - templ_7745c5c3_Err = absenceComponent(absentDay, false).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(c.ToString()) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 148, Col: 26} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "

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

") - 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: 152, Col: 25} - } - _, 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 - } + templ_7745c5c3_Err = newAbsenceComponent().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
") + templ_7745c5c3_Err = timeDayTypeSwitch(day, false).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -501,7 +426,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component { 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, 34, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -509,6 +434,70 @@ func defaultDayComponent(day models.IWorkDay) 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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var20 := templ.GetChildren(ctx) + if templ_7745c5c3_Var20 == nil { + templ_7745c5c3_Var20 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + switch day.Type() { + case models.DayTypeWorkday: + workDay, _ := day.(*models.WorkDay) + templ_7745c5c3_Err = workdayComponent(workDay).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case models.DayTypeAbsence: + absentDay, _ := day.(*models.Absence) + templ_7745c5c3_Err = absenceComponent(absentDay, fromCompound).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 = timeDayTypeSwitch(c, true).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "

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

") + 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 @@ -531,7 +520,7 @@ func workdayComponent(workDay *models.WorkDay) templ.Component { } ctx = templ.ClearChildren(ctx) if len(workDay.Bookings) < 1 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "

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

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

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -577,20 +566,20 @@ func holidayComponent(d models.IWorkDay) templ.Component { templ_7745c5c3_Var23 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var24 string templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(d.ToString()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 177, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 171, Col: 18} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/DB/initdb/03_create_user.sh b/DB/initdb/03_create_user.sh index fa78f94..42be086 100755 --- a/DB/initdb/03_create_user.sh +++ b/DB/initdb/03_create_user.sh @@ -5,9 +5,13 @@ 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, 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 -- 2.49.1 From 7fae75be757e05b2d80c822805ca0a3d72280155 Mon Sep 17 00:00:00 2001 From: tom_trgr Date: Wed, 24 Dec 2025 23:35:19 +0100 Subject: [PATCH 32/35] small refactor --- Backend/Makefile | 3 ++- Backend/endpoints/pdf-create.go | 5 +---- Backend/static/css/styles.css | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Backend/Makefile b/Backend/Makefile index 08eeb44..c7f2e96 100644 --- a/Backend/Makefile +++ b/Backend/Makefile @@ -13,4 +13,5 @@ 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 --minify --watch + npx --yes tailwindcss -i ./src/main.css -o ./static/css/styles.css --watch +#--minify diff --git a/Backend/endpoints/pdf-create.go b/Backend/endpoints/pdf-create.go index 2ccfad6..0ba4697 100644 --- a/Backend/endpoints/pdf-create.go +++ b/Backend/endpoints/pdf-create.go @@ -91,7 +91,7 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) { employes, err := models.GetUserByPersonalNrMulti(personalNumbers) if err != nil { slog.Warn("Error getting employes!", slog.Any("Error", err)) - // return + return } n := 0 @@ -102,9 +102,6 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) { } } employes = employes[:n] - if helper.GetEnv("GO_ENV", "production") == "debug" { - employes = append(employes, user) - } reportData := createReports(employes, startDate) diff --git a/Backend/static/css/styles.css b/Backend/static/css/styles.css index c5dc9bd..442fe2d 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */ -@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-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-orange-500:oklch(70.5% .213 47.604);--color-purple-600:oklch(55.8% .288 302.321);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-bold:700;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-accent:#0eaf23}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}body{background-color:#fff;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}}@layer components{.grid-main{grid-template-columns:4fr 3fr 3fr 1fr;align-items:stretch;display:grid}.grid-sub{grid-template-columns:subgrid;border-color:var(--color-neutral-400);grid-column:1/-1;transition:background-color .2s ease-in-out;display:grid}.grid-sub.responsive{flex-direction:column;display:flex}.grid-sub:hover{background-color:var(--color-neutral-200)}.grid-cell{padding:calc(var(--spacing)*2);border-color:var(--color-neutral-400)}.btn{cursor:pointer;border-radius:var(--radius-md);width:100%;color:var(--color-neutral-800);font-size:var(--text-sm);text-align:center;padding:calc(var(--spacing)*2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-neutral-800);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}input.btn,select.btn{transition-duration:.3s}.btn:hover{color:var(--color-white);background-color:var(--color-neutral-700)}.btn:disabled{opacity:.5;pointer-events:none}input.btn,select.btn{text-align:left}input.btn:hover,select.btn:hover{border-color:var(--color-neutral-300);background-color:var(--color-neutral-100);color:var(--color-neutral-800)}.edit-box{border-radius:var(--radius-md);border-color:var(--color-neutral-500);transition-property:background-color,border-color;transition-timing-function:var(--default-transition-timing-function)*2;transition-duration:var(--default-transition-duration);outline:none;overflow:hidden}.edit-box:is(:where(.group).edit *){border-width:1px}.edit-box:hover:is(:where(.group).edit *){background-color:var(--color-white);border-color:var(--color-neutral-300)}.edit-box input:focus{outline:none}div.edit{background-color:var(--color-neutral-300);border-width:1px}@media (min-width:48rem){.grid-main{grid-template-columns:repeat(5,1fr);margin:0 10%}.grid-sub.responsive{display:grid}.btn{padding-inline:calc(var(--spacing)*4)}}}@layer utilities{.\@container{container-type:inline-size}.absolute{position:absolute}.relative{position:relative}.top-1\/2{top:50%}.top-2\.5{top:calc(var(--spacing)*2.5)}.top-\[0\.125rem\]{top:.125rem}.right-1{right:calc(var(--spacing)*1)}.right-2\.5{right:calc(var(--spacing)*2.5)}.left-1\/2{left:50%}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-full{grid-column:1/-1}.mx-auto{margin-inline:auto}.-my-1{margin-block:calc(var(--spacing)*-1)}.mt-1{margin-top:calc(var(--spacing)*1)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.icon-\[material-symbols-light--cancel-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m8.4 16.308l3.6-3.6l3.6 3.6l.708-.708l-3.6-3.6l3.6-3.6l-.708-.708l-3.6 3.6l-3.6-3.6l-.708.708l3.6 3.6l-3.6 3.6zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--check-circle-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m10.562 15.908l6.396-6.396l-.708-.708l-5.688 5.688l-2.85-2.85l-.708.708zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--circle-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--delete-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M7.616 20q-.672 0-1.144-.472T6 18.385V6H5V5h4v-.77h6V5h4v1h-1v12.385q0 .69-.462 1.153T16.384 20zM17 6H7v12.385q0 .269.173.442t.443.173h8.769q.23 0 .423-.192t.192-.424zM9.808 17h1V8h-1zm3.384 0h1V8h-1zM7 6v13z'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--more-time\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M11.003 20q-1.666 0-3.123-.622t-2.545-1.71t-1.712-2.544T3 12.003t.622-3.123t1.711-2.546q1.09-1.089 2.545-1.711T11 4q.525 0 1.013.063T13 4.25V5.3q-.5-.15-.987-.225T11 5Q8.089 5 6.044 7.044T4 12t2.044 4.956T11 19t4.956-2.044T18 11.996q0-.271-.025-.554t-.094-.557h1.011q.05.236.08.538q.028.302.028.577q0 1.667-.622 3.122t-1.71 2.545q-1.089 1.088-2.544 1.71q-1.455.623-3.121.623m3.143-4.146L10.5 12.208V7h1v4.792l3.354 3.354zM18 8.884v-3h-3v-1h3v-3h1v3h3v1h-3v3z'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--motion-photos-paused-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9.808 14.616h1V9.385h-1zm3.384 0h1V9.385h-1zM12.003 21q-1.866 0-3.51-.705q-1.643-.706-2.859-1.915t-1.925-2.843T3 12.039q0-.905.167-1.778t.497-1.713l.78.78q-.219.65-.331 1.32T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.675 0-1.332.112t-1.3.332l-.776-.775q.789-.315 1.606-.492T11.885 3q1.887 0 3.546.701t2.894 1.926t1.955 2.866t.72 3.505t-.708 3.509t-1.924 2.859t-2.856 1.925t-3.509.709M5.923 6.808q-.356 0-.62-.265q-.264-.264-.264-.62t.264-.62t.62-.264t.62.264t.265.62t-.265.62t-.62.265M12 12'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--schedule-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m15.646 16.354l.708-.708l-3.854-3.854V7h-1v5.208zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.325 0 5.663-2.337T20 12t-2.337-5.663T12 4T6.337 6.338T4 12t2.338 5.663T12 20'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-5{width:calc(var(--spacing)*5);height:calc(var(--spacing)*5)}.h-2{height:calc(var(--spacing)*2)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-\[100vh\]{height:100vh}.h-full{height:100%}.w-2{width:calc(var(--spacing)*2)}.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\/10{width:90%}.w-\[2px\]{width:2px}.w-auto{width:auto}.w-full{width:100%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.grow-0{flex-grow:0}.grow-1{flex-grow:1}.basis-\[content\]{flex-basis:content}.-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\/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}.scroll-m-2{scroll-margin:calc(var(--spacing)*2)}.appearance-none{appearance:none}.break-after-page{break-after:page}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-\[3fr_2fr_2fr_2fr_3fr_3fr_3fr\]{grid-template-columns:3fr 2fr 2fr 2fr 3fr 3fr 3fr}.grid-cols-subgrid{grid-template-columns:subgrid}.grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.content-baseline{align-content:baseline}.content-end{align-content:flex-end}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}:where(.divide-x-1>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y-1>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-neutral-300>:not(:last-child)){border-color:var(--color-neutral-300)}.justify-self-end{justify-self:flex-end}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-r-1{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-neutral-300{border-color:var(--color-neutral-300)}.border-neutral-500{border-color:var(--color-neutral-500)}.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)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-300{background-color:var(--color-neutral-300)}.bg-neutral-400{background-color:var(--color-neutral-400)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-purple-600{background-color:var(--color-purple-600)}.bg-red-600{background-color:var(--color-red-600)}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.p-8{padding:calc(var(--spacing)*8)}.px-3{padding-inline:calc(var(--spacing)*3)}.py-2{padding-block:calc(var(--spacing)*2)}.py-4{padding-block:calc(var(--spacing)*4)}.text-center{text-align:center}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.whitespace-nowrap{white-space:nowrap}.text-accent{color:var(--color-accent)}.text-black{color:var(--color-black)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.text-red-500{color:var(--color-red-500)}.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}.opacity-0{opacity:0}.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,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,visibility,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.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));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.select-none{-webkit-user-select:none;user-select:none}:is(.\*\:text-center>*){text-align:center}@media not print{:is(.\*\:not-print\:p-2>*){padding:calc(var(--spacing)*2)}}@media (hover:hover){.group-hover\:text-black:is(:where(.group):hover *){color:var(--color-black)}.group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}}.group-\[\.edit\]\:ml-2:is(:where(.group).edit *){margin-left:calc(var(--spacing)*2)}.group-\[\.edit\]\:flex:is(:where(.group).edit *){display:flex}.group-\[\.edit\]\:hidden:is(:where(.group).edit *){display:none}.group-\[\.edit\]\:inline:is(:where(.group).edit *){display:inline}.group-\[\.edit\]\/button\:block:is(:where(.group\/button).edit *){display:block}.group-\[\.edit\]\/button\:hidden:is(:where(.group\/button).edit *){display:none}.peer-checked\:opacity-100:is(:where(.peer):checked~*){opacity:1}.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)}@media (hover:hover){.hover\:border-neutral-500:hover{border-color:var(--color-neutral-500)}.hover\:bg-neutral-200:hover{background-color:var(--color-neutral-200)}.hover\:bg-neutral-700:hover{background-color:var(--color-neutral-700)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-white:hover{color:var(--color-white)}}.focus\:bg-neutral-700:focus{background-color:var(--color-neutral-700)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:bg-neutral-700:active{background-color:var(--color-neutral-700)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}@media not all and (min-width:48rem){.max-md\:grid{display:grid}.max-md\:hidden{display:none}:where(.max-md\:divide-y-1>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}.max-md\:bg-neutral-300{background-color:var(--color-neutral-300)}}@media (min-width:48rem){.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-3{grid-column:span 3/span 3}.md\:mx-\[10\%\]{margin-inline:10%}.md\:block{display:block}.md\:hidden{display:none}.md\:inline{display:inline}.md\:w-1\/2{width:50%}.md\:flex-row{flex-direction:row}.md\:px-4{padding-inline:calc(var(--spacing)*4)}.md\:text-transparent{color:#0000}.group-\[\.edit\]\/button\:md\:block:is(:where(.group\/button).edit *){display:block}}@media (min-width:64rem){.lg\:hidden{display:none}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}:where(.lg\:divide-x-1>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}.lg\:border-0{border-style:var(--tw-border-style);border-width:0}}@container (min-width:80rem){.\@7xl\:grid{display:grid}.\@7xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}@media print{.print\:hidden{display:none}}}@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;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false} \ No newline at end of file +@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-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-orange-500:oklch(70.5% .213 47.604);--color-purple-600:oklch(55.8% .288 302.321);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-bold:700;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-accent:#0eaf23}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}body{background-color:#fff;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}}@layer components{.grid-main{grid-template-columns:4fr 3fr 3fr 1fr;align-items:stretch;display:grid}.grid-sub{grid-template-columns:subgrid;border-color:var(--color-neutral-400);grid-column:1/-1;transition:background-color .2s ease-in-out;display:grid}.grid-sub.responsive{flex-direction:column;display:flex}.grid-sub:hover{background-color:var(--color-neutral-200)}.grid-cell{padding:calc(var(--spacing)*2);border-color:var(--color-neutral-400)}.btn{cursor:pointer;border-radius:var(--radius-md);width:100%;color:var(--color-neutral-800);font-size:var(--text-sm);text-align:center;padding:calc(var(--spacing)*2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-neutral-800);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}input.btn,select.btn{transition-duration:.3s}.btn:hover{color:var(--color-white);background-color:var(--color-neutral-700)}.btn:disabled{opacity:.5;pointer-events:none}input.btn,select.btn{text-align:left}input.btn:hover,select.btn:hover{border-color:var(--color-neutral-300);background-color:var(--color-neutral-100);color:var(--color-neutral-800)}.edit-box{border-radius:var(--radius-md);border-color:var(--color-neutral-500);transition-property:background-color,border-color;transition-timing-function:var(--default-transition-timing-function)*2;transition-duration:var(--default-transition-duration);outline:none;overflow:hidden}.edit-box:is(:where(.group).edit *){border-width:1px}.edit-box:hover:is(:where(.group).edit *){background-color:var(--color-white);border-color:var(--color-neutral-300)}.edit-box input:focus{outline:none}div.edit{background-color:var(--color-neutral-300);border-width:1px}@media (min-width:48rem){.grid-main{grid-template-columns:repeat(5,1fr);margin:0 10%}.grid-sub.responsive{display:grid}.btn{padding-inline:calc(var(--spacing)*4)}}}@layer utilities{.\@container{container-type:inline-size}.absolute{position:absolute}.relative{position:relative}.top-1\/2{top:50%}.top-2\.5{top:calc(var(--spacing)*2.5)}.top-\[0\.125rem\]{top:.125rem}.right-1{right:calc(var(--spacing)*1)}.right-2\.5{right:calc(var(--spacing)*2.5)}.left-1\/2{left:50%}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-full{grid-column:1/-1}.mx-auto{margin-inline:auto}.-my-1{margin-block:calc(var(--spacing)*-1)}.mt-1{margin-top:calc(var(--spacing)*1)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.icon-\[material-symbols-light--cancel-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m8.4 16.308l3.6-3.6l3.6 3.6l.708-.708l-3.6-3.6l3.6-3.6l-.708-.708l-3.6 3.6l-3.6-3.6l-.708.708l3.6 3.6l-3.6 3.6zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--check-circle-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m10.562 15.908l6.396-6.396l-.708-.708l-5.688 5.688l-2.85-2.85l-.708.708zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--circle-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--delete-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M7.616 20q-.672 0-1.144-.472T6 18.385V6H5V5h4v-.77h6V5h4v1h-1v12.385q0 .69-.462 1.153T16.384 20zM17 6H7v12.385q0 .269.173.442t.443.173h8.769q.23 0 .423-.192t.192-.424zM9.808 17h1V8h-1zm3.384 0h1V8h-1zM7 6v13z'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--more-time\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M11.003 20q-1.666 0-3.123-.622t-2.545-1.71t-1.712-2.544T3 12.003t.622-3.123t1.711-2.546q1.09-1.089 2.545-1.711T11 4q.525 0 1.013.063T13 4.25V5.3q-.5-.15-.987-.225T11 5Q8.089 5 6.044 7.044T4 12t2.044 4.956T11 19t4.956-2.044T18 11.996q0-.271-.025-.554t-.094-.557h1.011q.05.236.08.538q.028.302.028.577q0 1.667-.622 3.122t-1.71 2.545q-1.089 1.088-2.544 1.71q-1.455.623-3.121.623m3.143-4.146L10.5 12.208V7h1v4.792l3.354 3.354zM18 8.884v-3h-3v-1h3v-3h1v3h3v1h-3v3z'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--motion-photos-paused-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9.808 14.616h1V9.385h-1zm3.384 0h1V9.385h-1zM12.003 21q-1.866 0-3.51-.705q-1.643-.706-2.859-1.915t-1.925-2.843T3 12.039q0-.905.167-1.778t.497-1.713l.78.78q-.219.65-.331 1.32T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.675 0-1.332.112t-1.3.332l-.776-.775q.789-.315 1.606-.492T11.885 3q1.887 0 3.546.701t2.894 1.926t1.955 2.866t.72 3.505t-.708 3.509t-1.924 2.859t-2.856 1.925t-3.509.709M5.923 6.808q-.356 0-.62-.265q-.264-.264-.264-.62t.264-.62t.62-.264t.62.264t.265.62t-.265.62t-.62.265M12 12'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--schedule-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m15.646 16.354l.708-.708l-3.854-3.854V7h-1v5.208zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.325 0 5.663-2.337T20 12t-2.337-5.663T12 4T6.337 6.338T4 12t2.338 5.663T12 20'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-5{width:calc(var(--spacing)*5);height:calc(var(--spacing)*5)}.h-2{height:calc(var(--spacing)*2)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-\[100vh\]{height:100vh}.h-full{height:100%}.w-2{width:calc(var(--spacing)*2)}.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\/10{width:90%}.w-\[2px\]{width:2px}.w-auto{width:auto}.w-full{width:100%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.grow-0{flex-grow:0}.grow-1{flex-grow:1}.basis-\[content\]{flex-basis:content}.-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\/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}.scroll-m-2{scroll-margin:calc(var(--spacing)*2)}.appearance-none{appearance:none}.break-after-page{break-after:page}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-\[3fr_2fr_2fr_2fr_3fr_3fr_3fr\]{grid-template-columns:3fr 2fr 2fr 2fr 3fr 3fr 3fr}.grid-cols-subgrid{grid-template-columns:subgrid}.grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.content-baseline{align-content:baseline}.content-end{align-content:flex-end}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}:where(.divide-x-1>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y-1>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-neutral-300>:not(:last-child)){border-color:var(--color-neutral-300)}.justify-self-end{justify-self:flex-end}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-r-1{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-neutral-300{border-color:var(--color-neutral-300)}.border-neutral-500{border-color:var(--color-neutral-500)}.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)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-300{background-color:var(--color-neutral-300)}.bg-neutral-400{background-color:var(--color-neutral-400)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-purple-600{background-color:var(--color-purple-600)}.bg-red-600{background-color:var(--color-red-600)}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.p-8{padding:calc(var(--spacing)*8)}.px-3{padding-inline:calc(var(--spacing)*3)}.py-2{padding-block:calc(var(--spacing)*2)}.py-4{padding-block:calc(var(--spacing)*4)}.text-center{text-align:center}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.whitespace-nowrap{white-space:nowrap}.text-accent{color:var(--color-accent)}.text-black{color:var(--color-black)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.text-red-500{color:var(--color-red-500)}.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}.opacity-0{opacity:0}.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,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,visibility,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.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));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.select-none{-webkit-user-select:none;user-select:none}:is(.\*\:text-center>*){text-align:center}@media not print{:is(.\*\:not-print\:p-2>*){padding:calc(var(--spacing)*2)}}@media (hover:hover){.group-hover\:text-black:is(:where(.group):hover *){color:var(--color-black)}.group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}}.group-\[\.edit\]\:ml-2:is(:where(.group).edit *){margin-left:calc(var(--spacing)*2)}.group-\[\.edit\]\:flex:is(:where(.group).edit *){display:flex}.group-\[\.edit\]\:hidden:is(:where(.group).edit *){display:none}.group-\[\.edit\]\:inline:is(:where(.group).edit *){display:inline}.group-\[\.edit\]\/button\:block:is(:where(.group\/button).edit *){display:block}.group-\[\.edit\]\/button\:hidden:is(:where(.group\/button).edit *){display:none}.peer-checked\:opacity-100:is(:where(.peer):checked~*){opacity:1}.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)}@media (hover:hover){.hover\:border-neutral-500:hover{border-color:var(--color-neutral-500)}.hover\:bg-neutral-200:hover{background-color:var(--color-neutral-200)}.hover\:bg-neutral-700:hover{background-color:var(--color-neutral-700)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-white:hover{color:var(--color-white)}}.focus\:bg-neutral-700:focus{background-color:var(--color-neutral-700)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:bg-neutral-700:active{background-color:var(--color-neutral-700)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}@media not all and (min-width:48rem){.max-md\:grid{display:grid}.max-md\:hidden{display:none}:where(.max-md\:divide-y-1>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}.max-md\:bg-neutral-300{background-color:var(--color-neutral-300)}}@media (min-width:48rem){.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-3{grid-column:span 3/span 3}.md\:mx-\[10\%\]{margin-inline:10%}.md\:block{display:block}.md\:hidden{display:none}.md\:inline{display:inline}.md\:w-1\/2{width:50%}.md\:flex-row{flex-direction:row}.md\:px-4{padding-inline:calc(var(--spacing)*4)}.md\:text-transparent{color:#0000}.group-\[\.edit\]\/button\:md\:block:is(:where(.group\/button).edit *){display:block}}@media (min-width:64rem){.lg\:hidden{display:none}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}:where(.lg\:divide-x-1>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}.lg\:border-0{border-style:var(--tw-border-style);border-width:0}}@container (min-width:80rem){.\@7xl\:grid{display:grid}.\@7xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}@media print{.print\:hidden{display:none}}}@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;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false} \ No newline at end of file -- 2.49.1 From 2b17eb6854487523cdd18fc04fd207a43f3ac1a8 Mon Sep 17 00:00:00 2001 From: tom_trgr Date: Fri, 2 Jan 2026 22:24:17 +0100 Subject: [PATCH 33/35] updates for presentation + starting update for newbooking component --- Backend/static/css/styles.css | 1292 +++++++++++++++++++- Backend/templates/headerComponent.templ | 3 +- Backend/templates/headerComponent_templ.go | 4 +- Backend/templates/pdf.templ | 14 +- Backend/templates/pdf_templ.go | 14 +- Backend/templates/timeComponents.templ | 13 +- Backend/templates/timeComponents_templ.go | 102 +- Backend/templates/timePage.templ | 102 +- Backend/templates/timePage_templ.go | 44 +- Docker/env.example | 1 + 10 files changed, 1438 insertions(+), 151 deletions(-) diff --git a/Backend/static/css/styles.css b/Backend/static/css/styles.css index 442fe2d..6d0ccdd 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -1,2 +1,1292 @@ /*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */ -@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-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-orange-500:oklch(70.5% .213 47.604);--color-purple-600:oklch(55.8% .288 302.321);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-bold:700;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-accent:#0eaf23}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}body{background-color:#fff;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}}@layer components{.grid-main{grid-template-columns:4fr 3fr 3fr 1fr;align-items:stretch;display:grid}.grid-sub{grid-template-columns:subgrid;border-color:var(--color-neutral-400);grid-column:1/-1;transition:background-color .2s ease-in-out;display:grid}.grid-sub.responsive{flex-direction:column;display:flex}.grid-sub:hover{background-color:var(--color-neutral-200)}.grid-cell{padding:calc(var(--spacing)*2);border-color:var(--color-neutral-400)}.btn{cursor:pointer;border-radius:var(--radius-md);width:100%;color:var(--color-neutral-800);font-size:var(--text-sm);text-align:center;padding:calc(var(--spacing)*2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-neutral-800);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}input.btn,select.btn{transition-duration:.3s}.btn:hover{color:var(--color-white);background-color:var(--color-neutral-700)}.btn:disabled{opacity:.5;pointer-events:none}input.btn,select.btn{text-align:left}input.btn:hover,select.btn:hover{border-color:var(--color-neutral-300);background-color:var(--color-neutral-100);color:var(--color-neutral-800)}.edit-box{border-radius:var(--radius-md);border-color:var(--color-neutral-500);transition-property:background-color,border-color;transition-timing-function:var(--default-transition-timing-function)*2;transition-duration:var(--default-transition-duration);outline:none;overflow:hidden}.edit-box:is(:where(.group).edit *){border-width:1px}.edit-box:hover:is(:where(.group).edit *){background-color:var(--color-white);border-color:var(--color-neutral-300)}.edit-box input:focus{outline:none}div.edit{background-color:var(--color-neutral-300);border-width:1px}@media (min-width:48rem){.grid-main{grid-template-columns:repeat(5,1fr);margin:0 10%}.grid-sub.responsive{display:grid}.btn{padding-inline:calc(var(--spacing)*4)}}}@layer utilities{.\@container{container-type:inline-size}.absolute{position:absolute}.relative{position:relative}.top-1\/2{top:50%}.top-2\.5{top:calc(var(--spacing)*2.5)}.top-\[0\.125rem\]{top:.125rem}.right-1{right:calc(var(--spacing)*1)}.right-2\.5{right:calc(var(--spacing)*2.5)}.left-1\/2{left:50%}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-full{grid-column:1/-1}.mx-auto{margin-inline:auto}.-my-1{margin-block:calc(var(--spacing)*-1)}.mt-1{margin-top:calc(var(--spacing)*1)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.icon-\[material-symbols-light--cancel-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m8.4 16.308l3.6-3.6l3.6 3.6l.708-.708l-3.6-3.6l3.6-3.6l-.708-.708l-3.6 3.6l-3.6-3.6l-.708.708l3.6 3.6l-3.6 3.6zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--check-circle-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m10.562 15.908l6.396-6.396l-.708-.708l-5.688 5.688l-2.85-2.85l-.708.708zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--circle-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--delete-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M7.616 20q-.672 0-1.144-.472T6 18.385V6H5V5h4v-.77h6V5h4v1h-1v12.385q0 .69-.462 1.153T16.384 20zM17 6H7v12.385q0 .269.173.442t.443.173h8.769q.23 0 .423-.192t.192-.424zM9.808 17h1V8h-1zm3.384 0h1V8h-1zM7 6v13z'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--more-time\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M11.003 20q-1.666 0-3.123-.622t-2.545-1.71t-1.712-2.544T3 12.003t.622-3.123t1.711-2.546q1.09-1.089 2.545-1.711T11 4q.525 0 1.013.063T13 4.25V5.3q-.5-.15-.987-.225T11 5Q8.089 5 6.044 7.044T4 12t2.044 4.956T11 19t4.956-2.044T18 11.996q0-.271-.025-.554t-.094-.557h1.011q.05.236.08.538q.028.302.028.577q0 1.667-.622 3.122t-1.71 2.545q-1.089 1.088-2.544 1.71q-1.455.623-3.121.623m3.143-4.146L10.5 12.208V7h1v4.792l3.354 3.354zM18 8.884v-3h-3v-1h3v-3h1v3h3v1h-3v3z'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--motion-photos-paused-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9.808 14.616h1V9.385h-1zm3.384 0h1V9.385h-1zM12.003 21q-1.866 0-3.51-.705q-1.643-.706-2.859-1.915t-1.925-2.843T3 12.039q0-.905.167-1.778t.497-1.713l.78.78q-.219.65-.331 1.32T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.675 0-1.332.112t-1.3.332l-.776-.775q.789-.315 1.606-.492T11.885 3q1.887 0 3.546.701t2.894 1.926t1.955 2.866t.72 3.505t-.708 3.509t-1.924 2.859t-2.856 1.925t-3.509.709M5.923 6.808q-.356 0-.62-.265q-.264-.264-.264-.62t.264-.62t.62-.264t.62.264t.265.62t-.265.62t-.62.265M12 12'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.icon-\[material-symbols-light--schedule-outline\]{width:1.25em;height:1.25em;-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m15.646 16.354l.708-.708l-3.854-3.854V7h-1v5.208zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.325 0 5.663-2.337T20 12t-2.337-5.663T12 4T6.337 6.338T4 12t2.338 5.663T12 20'/%3E%3C/svg%3E");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-5{width:calc(var(--spacing)*5);height:calc(var(--spacing)*5)}.h-2{height:calc(var(--spacing)*2)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-\[100vh\]{height:100vh}.h-full{height:100%}.w-2{width:calc(var(--spacing)*2)}.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\/10{width:90%}.w-\[2px\]{width:2px}.w-auto{width:auto}.w-full{width:100%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.grow-0{flex-grow:0}.grow-1{flex-grow:1}.basis-\[content\]{flex-basis:content}.-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\/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}.scroll-m-2{scroll-margin:calc(var(--spacing)*2)}.appearance-none{appearance:none}.break-after-page{break-after:page}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-\[3fr_2fr_2fr_2fr_3fr_3fr_3fr\]{grid-template-columns:3fr 2fr 2fr 2fr 3fr 3fr 3fr}.grid-cols-subgrid{grid-template-columns:subgrid}.grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.content-baseline{align-content:baseline}.content-end{align-content:flex-end}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}:where(.divide-x-1>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y-1>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-neutral-300>:not(:last-child)){border-color:var(--color-neutral-300)}.justify-self-end{justify-self:flex-end}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-r-1{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-neutral-300{border-color:var(--color-neutral-300)}.border-neutral-500{border-color:var(--color-neutral-500)}.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)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-300{background-color:var(--color-neutral-300)}.bg-neutral-400{background-color:var(--color-neutral-400)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-purple-600{background-color:var(--color-purple-600)}.bg-red-600{background-color:var(--color-red-600)}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.p-8{padding:calc(var(--spacing)*8)}.px-3{padding-inline:calc(var(--spacing)*3)}.py-2{padding-block:calc(var(--spacing)*2)}.py-4{padding-block:calc(var(--spacing)*4)}.text-center{text-align:center}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.whitespace-nowrap{white-space:nowrap}.text-accent{color:var(--color-accent)}.text-black{color:var(--color-black)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.text-red-500{color:var(--color-red-500)}.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}.opacity-0{opacity:0}.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,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,visibility,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.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));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.select-none{-webkit-user-select:none;user-select:none}:is(.\*\:text-center>*){text-align:center}@media not print{:is(.\*\:not-print\:p-2>*){padding:calc(var(--spacing)*2)}}@media (hover:hover){.group-hover\:text-black:is(:where(.group):hover *){color:var(--color-black)}.group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}}.group-\[\.edit\]\:ml-2:is(:where(.group).edit *){margin-left:calc(var(--spacing)*2)}.group-\[\.edit\]\:flex:is(:where(.group).edit *){display:flex}.group-\[\.edit\]\:hidden:is(:where(.group).edit *){display:none}.group-\[\.edit\]\:inline:is(:where(.group).edit *){display:inline}.group-\[\.edit\]\/button\:block:is(:where(.group\/button).edit *){display:block}.group-\[\.edit\]\/button\:hidden:is(:where(.group\/button).edit *){display:none}.peer-checked\:opacity-100:is(:where(.peer):checked~*){opacity:1}.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)}@media (hover:hover){.hover\:border-neutral-500:hover{border-color:var(--color-neutral-500)}.hover\:bg-neutral-200:hover{background-color:var(--color-neutral-200)}.hover\:bg-neutral-700:hover{background-color:var(--color-neutral-700)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-white:hover{color:var(--color-white)}}.focus\:bg-neutral-700:focus{background-color:var(--color-neutral-700)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:bg-neutral-700:active{background-color:var(--color-neutral-700)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}@media not all and (min-width:48rem){.max-md\:grid{display:grid}.max-md\:hidden{display:none}:where(.max-md\:divide-y-1>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}.max-md\:bg-neutral-300{background-color:var(--color-neutral-300)}}@media (min-width:48rem){.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-3{grid-column:span 3/span 3}.md\:mx-\[10\%\]{margin-inline:10%}.md\:block{display:block}.md\:hidden{display:none}.md\:inline{display:inline}.md\:w-1\/2{width:50%}.md\:flex-row{flex-direction:row}.md\:px-4{padding-inline:calc(var(--spacing)*4)}.md\:text-transparent{color:#0000}.group-\[\.edit\]\/button\:md\:block:is(:where(.group\/button).edit *){display:block}}@media (min-width:64rem){.lg\:hidden{display:none}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}:where(.lg\:divide-x-1>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}.lg\:border-0{border-style:var(--tw-border-style);border-width:0}}@container (min-width:80rem){.\@7xl\:grid{display:grid}.\@7xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}@media print{.print\:hidden{display:none}}}@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;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false} \ No newline at end of file +@layer properties; +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + --color-red-500: oklch(63.7% 0.237 25.331); + --color-red-600: oklch(57.7% 0.245 27.325); + --color-red-700: oklch(50.5% 0.213 27.518); + --color-orange-500: oklch(70.5% 0.213 47.604); + --color-purple-600: oklch(55.8% 0.288 302.321); + --color-slate-600: oklch(44.6% 0.043 257.281); + --color-slate-700: oklch(37.2% 0.044 257.287); + --color-slate-800: oklch(27.9% 0.041 260.031); + --color-neutral-100: oklch(97% 0 0); + --color-neutral-200: oklch(92.2% 0 0); + --color-neutral-300: oklch(87% 0 0); + --color-neutral-400: oklch(70.8% 0 0); + --color-neutral-500: oklch(55.6% 0 0); + --color-neutral-600: oklch(43.9% 0 0); + --color-neutral-700: oklch(37.1% 0 0); + --color-neutral-800: oklch(26.9% 0 0); + --color-black: #000; + --color-white: #fff; + --spacing: 0.25rem; + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --font-weight-bold: 700; + --radius-md: 0.375rem; + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + --color-accent: #0eaf23; + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} +@layer utilities { + .\@container { + container-type: inline-size; + } + .absolute { + position: absolute; + } + .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; + } + .col-span-3 { + grid-column: span 3 / span 3; + } + .col-span-full { + grid-column: 1 / -1; + } + .mx-auto { + margin-inline: auto; + } + .-my-1 { + margin-block: calc(var(--spacing) * -1); + } + .mt-1 { + margin-top: calc(var(--spacing) * 1); + } + .mb-1 { + margin-bottom: calc(var(--spacing) * 1); + } + .mb-2 { + margin-bottom: calc(var(--spacing) * 2); + } + .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; + height: 1.25em; + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m8.4 16.308l3.6-3.6l3.6 3.6l.708-.708l-3.6-3.6l3.6-3.6l-.708-.708l-3.6 3.6l-3.6-3.6l-.708.708l3.6 3.6l-3.6 3.6zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E"); + } + .icon-\[material-symbols-light--check-circle-outline\] { + display: inline-block; + width: 1.25em; + height: 1.25em; + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m10.562 15.908l6.396-6.396l-.708-.708l-5.688 5.688l-2.85-2.85l-.708.708zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E"); + } + .icon-\[material-symbols-light--circle-outline\] { + display: inline-block; + width: 1.25em; + height: 1.25em; + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8'/%3E%3C/svg%3E"); + } + .icon-\[material-symbols-light--delete-outline\] { + display: inline-block; + width: 1.25em; + height: 1.25em; + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M7.616 20q-.672 0-1.144-.472T6 18.385V6H5V5h4v-.77h6V5h4v1h-1v12.385q0 .69-.462 1.153T16.384 20zM17 6H7v12.385q0 .269.173.442t.443.173h8.769q.23 0 .423-.192t.192-.424zM9.808 17h1V8h-1zm3.384 0h1V8h-1zM7 6v13z'/%3E%3C/svg%3E"); + } + .icon-\[material-symbols-light--more-time\] { + display: inline-block; + width: 1.25em; + height: 1.25em; + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M11.003 20q-1.666 0-3.123-.622t-2.545-1.71t-1.712-2.544T3 12.003t.622-3.123t1.711-2.546q1.09-1.089 2.545-1.711T11 4q.525 0 1.013.063T13 4.25V5.3q-.5-.15-.987-.225T11 5Q8.089 5 6.044 7.044T4 12t2.044 4.956T11 19t4.956-2.044T18 11.996q0-.271-.025-.554t-.094-.557h1.011q.05.236.08.538q.028.302.028.577q0 1.667-.622 3.122t-1.71 2.545q-1.089 1.088-2.544 1.71q-1.455.623-3.121.623m3.143-4.146L10.5 12.208V7h1v4.792l3.354 3.354zM18 8.884v-3h-3v-1h3v-3h1v3h3v1h-3v3z'/%3E%3C/svg%3E"); + } + .icon-\[material-symbols-light--motion-photos-paused-outline\] { + display: inline-block; + width: 1.25em; + height: 1.25em; + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9.808 14.616h1V9.385h-1zm3.384 0h1V9.385h-1zM12.003 21q-1.866 0-3.51-.705q-1.643-.706-2.859-1.915t-1.925-2.843T3 12.039q0-.905.167-1.778t.497-1.713l.78.78q-.219.65-.331 1.32T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12t-2.325-5.675T12 4q-.675 0-1.332.112t-1.3.332l-.776-.775q.789-.315 1.606-.492T11.885 3q1.887 0 3.546.701t2.894 1.926t1.955 2.866t.72 3.505t-.708 3.509t-1.924 2.859t-2.856 1.925t-3.509.709M5.923 6.808q-.356 0-.62-.265q-.264-.264-.264-.62t.264-.62t.62-.264t.62.264t.265.62t-.265.62t-.62.265M12 12'/%3E%3C/svg%3E"); + } + .icon-\[material-symbols-light--schedule-outline\] { + display: inline-block; + width: 1.25em; + height: 1.25em; + background-color: currentColor; + -webkit-mask-image: var(--svg); + mask-image: var(--svg); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='m15.646 16.354l.708-.708l-3.854-3.854V7h-1v5.208zM12.003 21q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709M12 20q3.325 0 5.663-2.337T20 12t-2.337-5.663T12 4T6.337 6.338T4 12t2.338 5.663T12 20'/%3E%3C/svg%3E"); + } + .block { + display: block; + } + .flex { + display: flex; + } + .grid { + display: grid; + } + .hidden { + display: none; + } + .inline { + display: inline; + } + .inline-flex { + display: inline-flex; + } + .table { + display: table; + } + .size-2 { + width: calc(var(--spacing) * 2); + height: calc(var(--spacing) * 2); + } + .size-4 { + width: calc(var(--spacing) * 4); + height: calc(var(--spacing) * 4); + } + .size-5 { + width: calc(var(--spacing) * 5); + height: calc(var(--spacing) * 5); + } + .h-2 { + height: calc(var(--spacing) * 2); + } + .h-3 { + height: calc(var(--spacing) * 3); + } + .h-3\.5 { + height: calc(var(--spacing) * 3.5); + } + .h-4 { + height: calc(var(--spacing) * 4); + } + .h-5 { + height: calc(var(--spacing) * 5); + } + .h-8 { + height: calc(var(--spacing) * 8); + } + .h-10 { + height: calc(var(--spacing) * 10); + } + .h-\[100vh\] { + height: 100vh; + } + .h-full { + height: 100%; + } + .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%); + } + .w-\[2px\] { + width: 2px; + } + .w-auto { + width: auto; + } + .w-full { + width: 100%; + } + .flex-shrink { + flex-shrink: 1; + } + .flex-shrink-0 { + flex-shrink: 0; + } + .flex-grow { + flex-grow: 1; + } + .grow-0 { + flex-grow: 0; + } + .grow-1 { + flex-grow: 1; + } + .basis-\[content\] { + flex-basis: content; + } + .border-collapse { + border-collapse: collapse; + } + .-translate-x-1 { + --tw-translate-x: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .-translate-x-1\/2 { + --tw-translate-x: calc(calc(1/2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .-translate-y-1 { + --tw-translate-y: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .-translate-y-1\/2 { + --tw-translate-y: calc(calc(1/2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } + .cursor-pointer { + cursor: pointer; + } + .resize { + resize: both; + } + .scroll-m-2 { + scroll-margin: calc(var(--spacing) * 2); + } + .appearance-none { + appearance: none; + } + .break-after-page { + break-after: page; + } + .auto-rows-min { + grid-auto-rows: min-content; + } + .grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .grid-cols-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } + .grid-cols-\[3fr_2fr_2fr_2fr_3fr_3fr_3fr\] { + grid-template-columns: 3fr 2fr 2fr 2fr 3fr 3fr 3fr; + } + .grid-cols-subgrid { + grid-template-columns: subgrid; + } + .grid-rows-6 { + grid-template-rows: repeat(6, minmax(0, 1fr)); + } + .flex-col { + flex-direction: column; + } + .flex-row { + flex-direction: row; + } + .content-baseline { + align-content: baseline; + } + .content-end { + align-content: flex-end; + } + .items-center { + align-items: center; + } + .items-end { + align-items: flex-end; + } + .justify-around { + justify-content: space-around; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-4 { + gap: calc(var(--spacing) * 4); + } + .divide-x-1 { + :where(& > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); + } + } + .divide-y-1 { + :where(& > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(1px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + } + } + .divide-neutral-300 { + :where(& > :not(:last-child)) { + border-color: var(--color-neutral-300); + } + } + .justify-self-end { + justify-self: flex-end; + } + .overflow-hidden { + overflow: hidden; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .rounded-md { + border-radius: var(--radius-md); + } + .rounded-none { + border-radius: 0; + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-0 { + border-style: var(--tw-border-style); + border-width: 0px; + } + .border-r-0 { + border-right-style: var(--tw-border-style); + border-right-width: 0px; + } + .border-r-1 { + border-right-style: var(--tw-border-style); + border-right-width: 1px; + } + .border-b-0 { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 0px; + } + .border-dashed { + --tw-border-style: dashed; + border-style: dashed; + } + .border-neutral-300 { + border-color: var(--color-neutral-300); + } + .border-neutral-500 { + border-color: var(--color-neutral-500); + } + .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); + } + .bg-neutral-100 { + background-color: var(--color-neutral-100); + } + .bg-neutral-300 { + background-color: var(--color-neutral-300); + } + .bg-neutral-400 { + background-color: var(--color-neutral-400); + } + .bg-orange-500 { + background-color: var(--color-orange-500); + } + .bg-purple-600 { + background-color: var(--color-purple-600); + } + .bg-red-600 { + background-color: var(--color-red-600); + } + .mask-repeat { + mask-repeat: repeat; + } + .p-1 { + padding: calc(var(--spacing) * 1); + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-8 { + padding: calc(var(--spacing) * 8); + } + .px-3 { + padding-inline: calc(var(--spacing) * 3); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .py-4 { + padding-block: calc(var(--spacing) * 4); + } + .text-center { + text-align: center; + } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + .whitespace-nowrap { + white-space: nowrap; + } + .text-accent { + color: var(--color-accent); + } + .text-black { + color: var(--color-black); + } + .text-neutral-300 { + color: var(--color-neutral-300); + } + .text-neutral-500 { + color: var(--color-neutral-500); + } + .text-neutral-700 { + color: var(--color-neutral-700); + } + .text-neutral-800 { + color: var(--color-neutral-800); + } + .text-red-500 { + color: var(--color-red-500); + } + .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,); + } + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .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)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-300 { + --tw-duration: 300ms; + transition-duration: 300ms; + } + .select-none { + -webkit-user-select: none; + user-select: none; + } + .\*\:text-center { + :is(& > *) { + text-align: center; + } + } + .\*\:not-print\:p-2 { + :is(& > *) { + @media not print { + padding: calc(var(--spacing) * 2); + } + } + } + .group-hover\:text-black { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: var(--color-black); + } + } + } + .group-hover\:text-white { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: var(--color-white); + } + } + } + .group-\[\.edit\]\:ml-2 { + &:is(:where(.group):is(.edit) *) { + margin-left: calc(var(--spacing) * 2); + } + } + .group-\[\.edit\]\:flex { + &:is(:where(.group):is(.edit) *) { + display: flex; + } + } + .group-\[\.edit\]\:hidden { + &:is(:where(.group):is(.edit) *) { + display: none; + } + } + .group-\[\.edit\]\:inline { + &:is(:where(.group):is(.edit) *) { + display: inline; + } + } + .group-\[\.edit\]\/button\:block { + &:is(:where(.group\/button):is(.edit) *) { + display: block; + } + } + .group-\[\.edit\]\/button\:hidden { + &:is(:where(.group\/button):is(.edit) *) { + display: none; + } + } + .peer-checked\:opacity-100 { + &:is(:where(.peer):checked ~ *) { + opacity: 100%; + } + } + .placeholder\:text-neutral-400 { + &::placeholder { + color: var(--color-neutral-400); + } + } + .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) { + border-color: var(--color-neutral-500); + } + } + } + .hover\:bg-neutral-200 { + &:hover { + @media (hover: hover) { + background-color: var(--color-neutral-200); + } + } + } + .hover\:bg-neutral-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-neutral-700); + } + } + } + .hover\:bg-red-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-700); + } + } + } + .hover\:text-white { + &:hover { + @media (hover: hover) { + color: var(--color-white); + } + } + } + .focus\:bg-neutral-700 { + &:focus { + background-color: var(--color-neutral-700); + } + } + .focus\:outline-none { + &:focus { + --tw-outline-style: none; + outline-style: none; + } + } + .active\:bg-neutral-700 { + &:active { + background-color: var(--color-neutral-700); + } + } + .disabled\:pointer-events-none { + &:disabled { + pointer-events: none; + } + } + .disabled\:opacity-50 { + &:disabled { + opacity: 50%; + } + } + .max-md\:grid { + @media (width < 48rem) { + display: grid; + } + } + .max-md\:hidden { + @media (width < 48rem) { + display: none; + } + } + .max-md\:divide-y-1 { + @media (width < 48rem) { + :where(& > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(1px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + } + } + } + .max-md\:bg-neutral-300 { + @media (width < 48rem) { + background-color: var(--color-neutral-300); + } + } + .md\:col-span-1 { + @media (width >= 48rem) { + grid-column: span 1 / span 1; + } + } + .md\:col-span-3 { + @media (width >= 48rem) { + grid-column: span 3 / span 3; + } + } + .md\:mx-\[10\%\] { + @media (width >= 48rem) { + margin-inline: 10%; + } + } + .md\:block { + @media (width >= 48rem) { + display: block; + } + } + .md\:hidden { + @media (width >= 48rem) { + display: none; + } + } + .md\:inline { + @media (width >= 48rem) { + display: inline; + } + } + .md\:w-1\/2 { + @media (width >= 48rem) { + 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); + } + } + .md\:text-transparent { + @media (width >= 48rem) { + color: transparent; + } + } + .group-\[\.edit\]\/button\:md\:block { + &:is(:where(.group\/button):is(.edit) *) { + @media (width >= 48rem) { + display: block; + } + } + } + .lg\:hidden { + @media (width >= 64rem) { + display: none; + } + } + .lg\:grid-cols-1 { + @media (width >= 64rem) { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + } + .lg\:divide-x-1 { + @media (width >= 64rem) { + :where(& > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); + } + } + } + .lg\:border-0 { + @media (width >= 64rem) { + border-style: var(--tw-border-style); + border-width: 0px; + } + } + .\@7xl\:grid { + @container (width >= 80rem) { + display: grid; + } + } + .\@7xl\:grid-cols-5 { + @container (width >= 80rem) { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } + } + .print\:hidden { + @media print { + display: none; + } + } +} +@layer base { + body { + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + background-color: white; + } +} +@layer components { + .grid-main { + display: grid; + grid-template-columns: 4fr 3fr 3fr 1fr; + align-items: stretch; + } + .grid-sub { + display: grid; + grid-template-columns: subgrid; + grid-column: 1 / -1; + border-color: var(--color-neutral-400); + transition: background-color 0.2s ease-in-out; + } + .grid-sub.responsive { + display: flex; + flex-direction: column; + } + .grid-sub:hover { + background-color: var(--color-neutral-200); + } + .grid-cell { + padding: calc(var(--spacing) * 2); + border-color: var(--color-neutral-400); + } + .btn { + width: 100%; + cursor: pointer; + border-radius: var(--radius-md); + color: var(--color-neutral-800); + font-size: var(--text-sm); + text-align: center; + padding: calc(var(--spacing) * 2); + border-style: var(--tw-border-style); + border-width: 1px; + border-color: var(--color-neutral-800); + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var( --tw-ease, var(--default-transition-timing-function) ); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + input.btn, select.btn { + transition-duration: 300ms; + } + .btn:hover { + color: var(--color-white); + background-color: var(--color-neutral-700); + } + .btn:disabled { + opacity: 50%; + pointer-events: none; + } + input.btn, select.btn { + text-align: left; + } + input.btn:hover, select.btn:hover { + border-color: var(--color-neutral-300); + background-color: var(--color-neutral-100); + color: var(--color-neutral-800); + } + .edit-box { + border-radius: var(--radius-md); + overflow: hidden; + border-color: var(--color-neutral-500); + transition-property: background-color, border-color; + transition-timing-function: var(--default-transition-timing-function) * 2; + transition-duration: var(--default-transition-duration); + outline: none; + &:is(:where(.group):is(.edit) *) { + border-width: 1px; + } + } + .edit-box:hover { + &:is(:where(.group):is(.edit) *) { + background-color: var(--color-white); + border-color: var(--color-neutral-300); + } + } + .edit-box input:focus { + outline: none; + } + div.edit { + border-width: 1px; + background-color: var(--color-neutral-300); + } + @media (width >=48rem) { + .grid-main { + grid-template-columns: repeat(5, 1fr); + margin: 0 10%; + } + .grid-sub.responsive { + display: grid; + } + .btn { + padding-inline: calc(var(--spacing) * 4); + } + } +} +@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; + initial-value: 0; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-divide-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-duration { + syntax: "*"; + inherits: false; +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-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; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-duration: initial; + } + } +} diff --git a/Backend/templates/headerComponent.templ b/Backend/templates/headerComponent.templ index 53d044a..abcf693 100644 --- a/Backend/templates/headerComponent.templ +++ b/Backend/templates/headerComponent.templ @@ -1,11 +1,12 @@ package templates templ headerComponent() { + // {{ user := ctx.Value("user").(models.User) }}
Zeitverwaltung Abrechnung - PDF if true { + Monatsabrechnung Anwesenheit } Einstellungen diff --git a/Backend/templates/headerComponent_templ.go b/Backend/templates/headerComponent_templ.go index aa67d81..53d8123 100644 --- a/Backend/templates/headerComponent_templ.go +++ b/Backend/templates/headerComponent_templ.go @@ -29,12 +29,12 @@ func headerComponent() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Zeitverwaltung Abrechnung PDF ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Zeitverwaltung Abrechnung ") if templ_7745c5c3_Err != nil { 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/pdf.templ b/Backend/templates/pdf.templ index 7bed3a1..ab297a1 100644 --- a/Backend/templates/pdf.templ +++ b/Backend/templates/pdf.templ @@ -12,7 +12,7 @@ templ PDFForm(teamMembers []models.User) { @headerComponent()
-

PDF Abrechnung erstellen

+

Monatsabrechnung erstellen

Zeitraum wählen
@@ -46,9 +46,7 @@ templ PDFForm(teamMembers []models.User) { } templ CheckboxComponent(pNr int, label string) { - {{ - id := fmt.Sprintf("pdf-%d", pNr) - }} + {{ id := fmt.Sprintf("pdf-%d", pNr) }}