diff --git a/Backend/models/absence.go b/Backend/models/absence.go index 7c99e76..21150f6 100644 --- a/Backend/models/absence.go +++ b/Backend/models/absence.go @@ -13,8 +13,10 @@ package models // the absence data is based on the entries in the "abwesenheit" database table import ( + "database/sql" "encoding/json" "log" + "log/slog" "time" ) @@ -295,3 +297,24 @@ func (a *Absence) Delete() error { _, err = qStr.Exec(a.CounterId) return err } + +func (a *Absence) IsSubmittedAndAccepted() bool { + qStr, err := DB.Prepare(`SELECT bestaetigt from wochen_report WHERE $1 = ANY(abwesenheiten) AND $2 >= woche_start AND $2 < woche_start + INTERVAL '1 week';`) // @> array contains + if err != nil { + slog.Warn("Error when preparing SQL Statement", "error", err) + return false + } + defer qStr.Close() + var isSubmittedAndChecked bool = false + + err = qStr.QueryRow(a.CounterId, a.Date()).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 +} diff --git a/Backend/models/booking.go b/Backend/models/booking.go index 6c74f23..d406de4 100644 --- a/Backend/models/booking.go +++ b/Backend/models/booking.go @@ -95,27 +95,6 @@ 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/compoundDay.go b/Backend/models/compoundDay.go index 323cea7..c3aac27 100644 --- a/Backend/models/compoundDay.go +++ b/Backend/models/compoundDay.go @@ -17,6 +17,17 @@ type CompoundDay struct { DayParts []IWorkDay } +// IsSubmittedAndAccepted implements IWorkDay. +func (c *CompoundDay) IsSubmittedAndAccepted() bool { + var isSubmittedAndAccepted = true + for _, day := range c.DayParts { + _isSubmittedAndAccepted := day.IsSubmittedAndAccepted() + isSubmittedAndAccepted = isSubmittedAndAccepted && _isSubmittedAndAccepted + slog.Info("Result from IsSubmittedCheck", "Result", _isSubmittedAndAccepted, "compount", day.ToString()) + } + return isSubmittedAndAccepted +} + func NewCompondDay(date time.Time, dayParts ...IWorkDay) *CompoundDay { return &CompoundDay{Day: date, DayParts: dayParts} } diff --git a/Backend/models/iworkday.go b/Backend/models/iworkday.go index 2f7d038..3f96166 100644 --- a/Backend/models/iworkday.go +++ b/Backend/models/iworkday.go @@ -23,6 +23,7 @@ type IWorkDay interface { GetTimes(User, WorktimeBase, bool) (work, pause, overtime time.Duration) GetOvertime(User, WorktimeBase, bool) time.Duration IsEmpty() bool + IsSubmittedAndAccepted() bool } type DayType int diff --git a/Backend/models/publicHoliday.go b/Backend/models/publicHoliday.go index 1a6707a..bc1b11d 100644 --- a/Backend/models/publicHoliday.go +++ b/Backend/models/publicHoliday.go @@ -19,6 +19,11 @@ type PublicHoliday struct { worktime int8 } +// IsSubmittedAndAccepted implements IWorkDay. +func (p *PublicHoliday) IsSubmittedAndAccepted() bool { + return true +} + // IsEmpty implements [IWorkDay]. func (p *PublicHoliday) IsEmpty() bool { return false diff --git a/Backend/models/user.go b/Backend/models/user.go index 605687c..5389c33 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -292,10 +292,42 @@ func (u *User) GetNextWeek() WorkWeek { func (u *User) GetLastWorkWeekSubmission() time.Time { var lastSub time.Time qStr, err := DB.Prepare(` - SELECT COALESCE( - (SELECT woche_start + INTERVAL '1 week' FROM wochen_report WHERE personal_nummer = $1 ORDER BY woche_start DESC LIMIT 1), - (SELECT timestamp FROM anwesenheit WHERE card_uid = $2 ORDER BY timestamp LIMIT 1) - ) AS letzte_buchung; + SELECT new_week +FROM ( + -- Highest priority + SELECT + woche_start AS new_week, + 1 AS priority + FROM wochen_report + WHERE personal_nummer = $1 + AND bestaetigt IS NULL + + UNION ALL + + -- Fallback if #1 returns nothing + SELECT + woche_start + INTERVAL '1 week' AS new_week, + 2 AS priority + FROM wochen_report wo + WHERE personal_nummer = $1 + AND NOT EXISTS ( + SELECT 1 + FROM wochen_report wi + WHERE wi.woche_start = wo.woche_start + INTERVAL '1 week' + AND wi.personal_nummer = wo.personal_nummer + ) + + UNION ALL + + -- Final fallback + SELECT + timestamp AS new_week, + 3 AS priority + FROM anwesenheit + WHERE card_uid = $2 +) t +ORDER BY priority, new_week +LIMIT 1; `) if err != nil { slog.Debug("Error preparing query statement.", "error", err) diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index 912e519..3e9eb05 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -7,12 +7,15 @@ package models import ( "arbeitszeitmessung/helper" + "database/sql" "encoding/json" "fmt" "log" "log/slog" "sort" "time" + + "github.com/lib/pq" ) type WorkDay struct { @@ -425,3 +428,38 @@ func (d *WorkDay) GetDayProgress(u User) int8 { progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100 return int8(progress) } + +func (d *WorkDay) IsSubmittedAndAccepted() bool { + var isKurzArbeitAccepted bool + if d.IsKurzArbeit() { + isKurzArbeitAccepted = d.kurzArbeitAbsence.IsSubmittedAndAccepted() + } + + if d.IsEmpty() { + return isKurzArbeitAccepted + } + + qStr, err := DB.Prepare(`SELECT bestaetigt from wochen_report WHERE anwesenheiten @> $1 AND $2 >= woche_start AND $2 < woche_start + INTERVAL '1 week';`) // @> array contains + if err != nil { + slog.Warn("Error when preparing SQL Statement", "error", err) + return false + } + + defer qStr.Close() + var isSubmittedAndChecked bool = false + + var bookingsIds []int + for _, booking := range d.Bookings { + bookingsIds = append(bookingsIds, booking.CounterId) + } + + err = qStr.QueryRow(pq.Array(bookingsIds), d.Date()).Scan(&isSubmittedAndChecked) + if err == sql.ErrNoRows { + return false + } + + if err != nil { + slog.Warn("Unexpected error when executing SQL Statement", "error", err, "BookingsIds", bookingsIds) + } + return isSubmittedAndChecked +} diff --git a/Backend/models/workWeek.go b/Backend/models/workWeek.go index ef7d284..1dfcbd3 100644 --- a/Backend/models/workWeek.go +++ b/Backend/models/workWeek.go @@ -34,6 +34,7 @@ type WeekStatus int8 const ( WeekStatusNone WeekStatus = iota + WeekStatusCorrected WeekStatusSent WeekStatusAccepted WeekStatusDifferences @@ -86,25 +87,31 @@ func (w *WorkWeek) CheckStatus() WeekStatus { log.Println("Cannot access Database!") return w.Status } - qStr, err := DB.Prepare(`SELECT bestaetigt FROM wochen_report WHERE woche_start = $1::DATE AND personal_nummer = $2;`) + qStr, err := DB.Prepare(`SELECT bestaetigt, id FROM wochen_report WHERE woche_start = $1::DATE AND personal_nummer = $2;`) if err != nil { log.Println("Error preparing SQL statement", err) return w.Status } + defer qStr.Close() - var beastatigt bool - err = qStr.QueryRow(w.WeekStart, w.User.PersonalNummer).Scan(&beastatigt) + var beastatigt sql.NullBool + err = qStr.QueryRow(w.WeekStart, w.User.PersonalNummer).Scan(&beastatigt, &w.Id) if err == sql.ErrNoRows { return w.Status } + slog.Info("Bestätigt query res", "Best", beastatigt, "week", w.Id) if err != nil { log.Println("Error querying database", err) return w.Status } - if beastatigt { + switch { + case beastatigt.Bool: w.Status = WeekStatusAccepted - } else { + case beastatigt.Valid: w.Status = WeekStatusSent + default: + w.Status = WeekStatusCorrected + } return w.Status } @@ -206,23 +213,33 @@ func (w *WorkWeek) SendWeek() error { 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), anwesenheiten=$5, abwesenheiten=$6 WHERE personal_nummer = $1 AND woche_start = $2;`) - if err != nil { - slog.Warn("Error preparing SQL statement", "error", err) - return err - } - } else { + switch w.CheckStatus() { + case WeekStatusNone: 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 { slog.Warn("Error preparing SQL statement", "error", err) return err } + + case WeekStatusCorrected: + 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 { + slog.Warn("Error preparing SQL statement", "error", err) + return err + } + + case WeekStatusSent, WeekStatusAccepted: + qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = null WHERE personal_nummer = $1 AND woche_start = $2 AND ($3::numeric IS NULL OR TRUE) AND ($4::numeric IS NULL OR TRUE) AND ($5::int[] IS NULL OR TRUE) AND ($6::int[] IS NULL OR TRUE);`) + if err != nil { + slog.Warn("Error preparing SQL statement", "error", err) + return err + } + } _, 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) + slog.Error("Error executing query!", "error", err) return err } return nil diff --git a/Backend/static/css/styles.css b/Backend/static/css/styles.css index 553198f..b26c407 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -20,7 +20,6 @@ --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; @@ -30,8 +29,6 @@ --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; @@ -253,6 +250,12 @@ .-my-1 { margin-block: calc(var(--spacing) * -1); } + .my-2 { + margin-block: calc(var(--spacing) * 2); + } + .my-4 { + margin-block: calc(var(--spacing) * 4); + } .mt-1 { margin-top: calc(var(--spacing) * 1); } @@ -320,6 +323,32 @@ 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--edit-calendar-rounded\] { + 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='M5.616 21q-.691 0-1.153-.462T4 19.385V6.615q0-.69.463-1.152T5.616 5h1.769V3.308q0-.23.155-.384q.156-.155.386-.155t.383.155t.153.384V5h7.154V3.27q0-.213.143-.357q.144-.144.357-.144t.356.144t.144.356V5h1.769q.69 0 1.153.463T20 6.616v4.601q0 .213-.144.356t-.357.144t-.356-.144t-.143-.356v-.602H5v8.77q0 .23.192.423t.423.192h5.731q.213 0 .357.144t.143.357t-.143.356t-.357.143zm8.615-.808V19.12q0-.153.056-.296q.055-.144.186-.275l5.09-5.065q.149-.13.306-.19t.315-.062q.172 0 .338.064q.166.065.301.194l.925.944q.123.148.188.308q.064.159.064.319t-.052.322t-.2.31l-5.065 5.066q-.131.13-.275.186q-.143.056-.297.056h-1.073q-.343 0-.575-.232t-.232-.576m5.96-4.177l.925-.956l-.925-.944l-.95.95z'/%3E%3C/svg%3E"); + } + .icon-\[material-symbols-light--lock\] { + 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='M6.616 21q-.667 0-1.141-.475T5 19.386v-8.77q0-.666.475-1.14T6.615 9H8V7q0-1.671 1.165-2.835Q10.329 3 12 3t2.836 1.165T16 7v2h1.385q.666 0 1.14.475t.475 1.14v8.77q0 .666-.475 1.14t-1.14.475zM12 16.5q.633 0 1.066-.434q.434-.433.434-1.066t-.434-1.066T12 13.5t-1.066.434Q10.5 14.367 10.5 15t.434 1.066q.433.434 1.066.434M9 9h6V7q0-1.25-.875-2.125T12 4t-2.125.875T9 7z'/%3E%3C/svg%3E"); + } .icon-\[material-symbols-light--more-time\] { display: inline-block; width: 1.25em; @@ -395,6 +424,18 @@ width: calc(var(--spacing) * 5); height: calc(var(--spacing) * 5); } + .size-6 { + width: calc(var(--spacing) * 6); + height: calc(var(--spacing) * 6); + } + .size-8 { + width: calc(var(--spacing) * 8); + height: calc(var(--spacing) * 8); + } + .size-10 { + width: calc(var(--spacing) * 10); + height: calc(var(--spacing) * 10); + } .h-2 { height: calc(var(--spacing) * 2); } @@ -633,6 +674,9 @@ .p-2 { padding: calc(var(--spacing) * 2); } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } .px-3 { padding-inline: calc(var(--spacing) * 3); } @@ -660,9 +704,6 @@ .whitespace-nowrap { white-space: nowrap; } - .\!text-red-500 { - color: var(--color-red-500) !important; - } .text-accent { color: var(--color-accent); } diff --git a/Backend/templates/reportPage.templ b/Backend/templates/reportPage.templ index 83e9191..8942fc0 100644 --- a/Backend/templates/reportPage.templ +++ b/Backend/templates/reportPage.templ @@ -47,6 +47,12 @@ templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
if !onlyAccept {
+ if week.CheckStatus() == models.WeekStatusCorrected { + +
+ laufende Korrektur +
+ } @statusCheckMark(week.CheckStatus(), models.WeekStatusSent) Gesendet diff --git a/Backend/templates/timeComponents.templ b/Backend/templates/timeComponents.templ index e551102..711db60 100644 --- a/Backend/templates/timeComponents.templ +++ b/Backend/templates/timeComponents.templ @@ -6,16 +6,22 @@ import ( "time" ) -templ changeButtonComponent(id string, workDay bool) { - - +templ changeButtonComponent(id string, workDay bool, disabled bool) { + if disabled { + + } else { + + + } } templ newAbsenceComponent() { @@ -85,9 +91,6 @@ templ bookingComponent(booking models.Booking) { fehlerhafte Buchung, wird nicht zur Berechnung verwendet! }

- if booking.IsSubmittedAndChecked() { -

submitted

- }
} diff --git a/Backend/templates/timePage.templ b/Backend/templates/timePage.templ index 4ac2e6c..0666ab3 100644 --- a/Backend/templates/timePage.templ +++ b/Backend/templates/timePage.templ @@ -142,8 +142,11 @@ templ defaultDayComponent(day models.IWorkDay) {
-
- @changeButtonComponent("time-"+day.Date().Format(time.DateOnly), true) +
+ @changeButtonComponent("time-"+day.Date().Format(time.DateOnly), true, day.IsSubmittedAndAccepted()) + if day.IsSubmittedAndAccepted() { + + }
} diff --git a/DBB/initdb/01_create_user.sh b/DBB/initdb/01_create_user.sh deleted file mode 100755 index 91c3259..0000000 --- a/DBB/initdb/01_create_user.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -set -e # Exit on error - -echo "Creating PostgreSQL user and setting permissions... $POSTGRES_USER for API user $POSTGRES_API_USER" - - - -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL - CREATE ROLE migrate LOGIN ENCRYPTED PASSWORD '$POSTGRES_PASSWORD'; - GRANT USAGE, CREATE ON SCHEMA public TO migrate; - GRANT CONNECT ON DATABASE arbeitszeitmessung TO migrate; -EOSQL - -# psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL - -# 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 - -echo "User creation and permissions setup complete!" - - -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL - --- privilege roles -DO \$\$ -BEGIN - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_base') THEN - CREATE ROLE app_base NOLOGIN; - END IF; -END -\$\$; - --- dynamic login role -DO \$\$ -BEGIN - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_API_USER') THEN - CREATE ROLE $POSTGRES_API_USER - LOGIN - ENCRYPTED PASSWORD '$POSTGRES_API_PASS'; - END IF; -END -\$\$; - --- grant base privileges -GRANT app_base TO $POSTGRES_API_USER; -GRANT CONNECT ON DATABASE $POSTGRES_DB TO $POSTGRES_API_USER; -GRANT USAGE ON SCHEMA public TO $POSTGRES_API_USER; - -CREATE EXTENSION IF NOT EXISTS pgcrypto; - -EOSQL - -# psql -v ON_ERROR_STOP=1 --username root --dbname arbeitszeitmessung