diff --git a/Backend/models/booking.go b/Backend/models/booking.go index 6704913..6c74f23 100644 --- a/Backend/models/booking.go +++ b/Backend/models/booking.go @@ -36,6 +36,7 @@ type Booking struct { Timestamp time.Time `json:"timestamp"` CounterId int `json:"counter_id"` BookingType BookingType `json:"anwesenheit_typ"` + Valid bool `json:"valid"` } type IDatabase interface { @@ -252,12 +253,13 @@ func (b *Booking) Update(nb Booking) { func checkLastBooking(b Booking) bool { var check_in_out int slog.Info("Checking with timestamp:", "timestamp", b.Timestamp.String()) - stmt, err := DB.Prepare((`SELECT check_in_out FROM "anwesenheit" WHERE "card_uid" = $1 AND "timestamp"::DATE <= $2::DATE ORDER BY "timestamp" DESC LIMIT 1;`)) + stmt, err := DB.Prepare((`SELECT check_in_out FROM "anwesenheit" WHERE "card_uid" = $1 AND "timestamp" <= $2 ORDER BY "timestamp" DESC LIMIT 1;`)) if err != nil { log.Fatalf("Error preparing query: %v", err) return false } err = stmt.QueryRow(b.CardUID, b.Timestamp).Scan(&check_in_out) + slog.Info("Checking last bookings check_in_out", "Check", check_in_out) if err == sql.ErrNoRows { return true } diff --git a/Backend/models/workDay.go b/Backend/models/workDay.go index 4477323..912e519 100644 --- a/Backend/models/workDay.go +++ b/Backend/models/workDay.go @@ -186,97 +186,148 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay { var workSec, pauseSec float64 qStr, err := DB.Prepare(` - WITH all_days AS ( - SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date), - normalized_bookings AS ( - SELECT * - FROM ( - SELECT - a.card_uid, - a.timestamp, - a.timestamp::DATE AS work_date, - a.check_in_out, - a.counter_id, - a.anwesenheit_typ, - sat.anwesenheit_name AS anwesenheit_typ_name, - LAG(a.check_in_out) OVER ( - PARTITION BY a.card_uid, a.timestamp::DATE - ORDER BY a.timestamp - ) AS prev_check - FROM anwesenheit a - LEFT JOIN s_anwesenheit_typen sat - ON a.anwesenheit_typ = sat.anwesenheit_id - WHERE a.card_uid = $1 - AND a.timestamp::DATE >= $2 - AND a.timestamp::DATE <= $3 - ) t - WHERE prev_check IS NULL OR prev_check <> check_in_out - ), - ordered_bookings AS ( - SELECT - *, - LAG(timestamp) OVER ( - PARTITION BY card_uid, work_date - ORDER BY timestamp - ) AS prev_timestamp - FROM normalized_bookings - ) - SELECT - d.work_date, - COALESCE(MIN(b.timestamp), NOW()) AS time_from, - COALESCE(MAX(b.timestamp), NOW()) AS time_to, - COALESCE( - EXTRACT(EPOCH FROM SUM( - CASE - WHEN b.prev_check IN (1, 3) AND b.check_in_out IN (2, 4, 254) - THEN b.timestamp - b.prev_timestamp - ELSE INTERVAL '0' - END - )), 0 - ) AS total_work_seconds, - COALESCE( - EXTRACT(EPOCH FROM SUM( - CASE - WHEN b.prev_check IN (2, 4, 254) AND b.check_in_out IN (1, 3) - THEN b.timestamp - b.prev_timestamp - ELSE INTERVAL '0' - END - )), 0 - ) AS total_pause_seconds, - COALESCE(jsonb_agg(jsonb_build_object( - 'check_in_out', b.check_in_out, - 'timestamp', b.timestamp, - 'counter_id', b.counter_id, - 'anwesenheit_typ', b.anwesenheit_typ, - 'anwesenheit_typ', jsonb_build_object( - 'anwesenheit_id', b.anwesenheit_typ, - 'anwesenheit_name', b.anwesenheit_typ_name - ) - ) ORDER BY b.timestamp), '[]'::jsonb) AS bookings - FROM all_days d - LEFT JOIN ordered_bookings b ON d.work_date = b.work_date - GROUP BY d.work_date - ORDER BY d.work_date ASC;`) + WITH + all_days AS ( + SELECT + generate_series( + $2 ::DATE, + $3 ::DATE - INTERVAL '1 day', + INTERVAL '1 day' + )::DATE AS work_date + ), + all_bookings AS ( + SELECT + a.card_uid, + a.timestamp, + a.timestamp::DATE AS work_date, + a.check_in_out, + a.counter_id, + a.anwesenheit_typ, + sat.anwesenheit_name AS anwesenheit_typ_name, + LAG(a.check_in_out) OVER ( + PARTITION BY + a.card_uid, + a.timestamp::DATE + ORDER BY + a.timestamp + ) AS prev_check, + LAG(a.timestamp) OVER ( + PARTITION BY + a.card_uid, + a.timestamp::DATE + ORDER BY + a.timestamp + ) AS prev_timestamp + FROM + anwesenheit a + LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id + WHERE + a.card_uid = $1 + AND a.timestamp::DATE >= $2::DATE + AND a.timestamp::DATE <= $3::DATE + ), + normalized_bookings AS ( + SELECT + * + FROM + all_bookings + WHERE + prev_check IS NULL + OR prev_check <> check_in_out + ) +SELECT + d.work_date, + COALESCE(MIN(b.timestamp), NOW()) AS time_from, + COALESCE(MAX(b.timestamp), NOW()) AS time_to, + EXTRACT( + EPOCH + FROM + SUM( + CASE + WHEN b.prev_check IN (1, 3) + AND b.check_in_out IN (2, 4, 254) THEN b.timestamp - b.prev_timestamp + ELSE INTERVAL '0' + END + ) + ) AS total_work_seconds, + EXTRACT( + EPOCH + FROM + SUM( + CASE + WHEN b.prev_check IN (2, 4, 254) + AND b.check_in_out IN (1, 3) THEN b.timestamp - b.prev_timestamp + ELSE INTERVAL '0' + END + ) + ) AS total_pause_seconds, + jsonb_agg( + jsonb_build_object( + 'check_in_out', + b.check_in_out, + 'valid', + coalesce(b.check_in_out != b.prev_check, true), + 'timestamp', + b.timestamp, + 'counter_id', + b.counter_id, + 'anwesenheit_typ', + jsonb_build_object( + 'anwesenheit_id', + b.anwesenheit_typ, + 'anwesenheit_name', + b.anwesenheit_typ_name + ) + ) + ORDER BY + b.timestamp + ) FILTER ( + WHERE + b.card_uid IS NOT NULL + ) AS bookings +FROM + all_days d + LEFT JOIN all_bookings b ON b.work_date = d.work_date +GROUP BY + d.work_date; + `) // qStr, err := DB.Prepare(` // WITH all_days AS ( // SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date), - // ordered_bookings AS ( - // SELECT - // a.timestamp::DATE AS work_date, - // a.timestamp, - // a.check_in_out, - // a.counter_id, - // a.anwesenheit_typ, - // sat.anwesenheit_name AS anwesenheit_typ_name, - // LAG(a.timestamp) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_timestamp, - // LAG(a.check_in_out) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_check - // FROM anwesenheit a - // LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id - // WHERE a.card_uid = $1 - // AND a.timestamp::DATE >= $2 - // AND a.timestamp::DATE <= $3 - // ) + // normalized_bookings AS ( + // SELECT * + // FROM ( + // SELECT + // a.card_uid, + // a.timestamp, + // a.timestamp::DATE AS work_date, + // a.check_in_out, + // a.counter_id, + // a.anwesenheit_typ, + // sat.anwesenheit_name AS anwesenheit_typ_name, + // LAG(a.check_in_out) OVER ( + // PARTITION BY a.card_uid, a.timestamp::DATE + // ORDER BY a.timestamp + // ) AS prev_check + // FROM anwesenheit a + // LEFT JOIN s_anwesenheit_typen sat + // ON a.anwesenheit_typ = sat.anwesenheit_id + // WHERE a.card_uid = $1 + // AND a.timestamp::DATE >= $2 + // AND a.timestamp::DATE <= $3 + // ) t + // WHERE prev_check IS NULL OR prev_check <> check_in_out + // ), + // ordered_bookings AS ( + // SELECT + // *, + // LAG(timestamp) OVER ( + // PARTITION BY card_uid, work_date + // ORDER BY timestamp + // ) AS prev_timestamp + // FROM normalized_bookings + // ) // SELECT // d.work_date, // COALESCE(MIN(b.timestamp), NOW()) AS time_from, @@ -330,26 +381,29 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay { var workDay WorkDay var bookings []byte if err := rows.Scan(&workDay.Day, &workDay.TimeFrom, &workDay.TimeTo, &workSec, &pauseSec, &bookings); err != nil { - log.Println("Error scanning row!", err) + slog.Error("Error scanning row!", "Error", err) return workDays } workDay.workTime = time.Duration(workSec * float64(time.Second)) workDay.pauseTime = time.Duration(pauseSec * float64(time.Second)) - err = json.Unmarshal(bookings, &workDay.Bookings) - if err != nil { - log.Println("Error parsing bookings JSON!", err) - return nil + if bookings != nil { + err = json.Unmarshal(bookings, &workDay.Bookings) + if err != nil { + slog.Error("Error parsing bookings JSON!", "Error", err, "Json", bookings) + return nil + } } + // better empty day handling - if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 { - workDay.Bookings = []Booking{} - } + // if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 { + // workDay.Bookings = []Booking{} + // } if len(workDay.Bookings) >= 1 || !helper.IsWeekend(workDay.Date()) { workDays = append(workDays, workDay) } } if err = rows.Err(); err != nil { - log.Println("Error in workday rows!", err) + slog.Error("Error in workday rows!", "Error", err) return workDays } return workDays diff --git a/Backend/static/css/styles.css b/Backend/static/css/styles.css index 4b2dd1d..553198f 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -205,9 +205,15 @@ .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); } @@ -217,9 +223,15 @@ .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%); } @@ -350,6 +362,9 @@ .block { display: block; } + .contents { + display: contents; + } .flex { display: flex; } @@ -383,6 +398,9 @@ .h-2 { height: calc(var(--spacing) * 2); } + .h-3 { + height: calc(var(--spacing) * 3); + } .h-3\.5 { height: calc(var(--spacing) * 3.5); } @@ -407,6 +425,9 @@ .w-2 { width: calc(var(--spacing) * 2); } + .w-3 { + width: calc(var(--spacing) * 3); + } .w-3\.5 { width: calc(var(--spacing) * 3.5); } @@ -416,6 +437,9 @@ .w-5 { width: calc(var(--spacing) * 5); } + .w-9 { + width: calc(var(--spacing) * 9); + } .w-9\/10 { width: calc(9/10 * 100%); } @@ -428,6 +452,9 @@ .w-full { width: 100%; } + .flex-shrink { + flex-shrink: 1; + } .flex-shrink-0 { flex-shrink: 0; } @@ -443,10 +470,21 @@ .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); @@ -457,33 +495,21 @@ .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; } @@ -534,11 +560,6 @@ 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; } @@ -565,18 +586,10 @@ 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; @@ -587,9 +600,6 @@ .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); } @@ -614,15 +624,15 @@ .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); } @@ -635,10 +645,6 @@ .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)); @@ -654,15 +660,15 @@ .whitespace-nowrap { white-space: nowrap; } + .\!text-red-500 { + color: var(--color-red-500) !important; + } .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); } @@ -690,9 +696,16 @@ .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,); } @@ -719,18 +732,6 @@ -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) { @@ -1024,7 +1025,7 @@ 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-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } input.btn, select.btn { @@ -1138,6 +1139,11 @@ syntax: "*"; inherits: false; } +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} @property --tw-blur { syntax: "*"; inherits: false; @@ -1210,6 +1216,7 @@ --tw-border-style: solid; --tw-divide-y-reverse: 0; --tw-font-weight: initial; + --tw-outline-style: solid; --tw-blur: initial; --tw-brightness: initial; --tw-contrast: initial; diff --git a/Backend/templates/timeComponents.templ b/Backend/templates/timeComponents.templ index 7f2d066..e551102 100644 --- a/Backend/templates/timeComponents.templ +++ b/Backend/templates/timeComponents.templ @@ -77,10 +77,13 @@ templ newBookingComponent(d models.IWorkDay) { templ bookingComponent(booking models.Booking) {
+
{ booking.Timestamp.Format("15:04") } { booking.GetBookingType() } + if !booking.Valid { + fehlerhafte Buchung, wird nicht zur Berechnung verwendet! + }
if booking.IsSubmittedAndChecked() {submitted
@@ -89,10 +92,10 @@ templ bookingComponent(booking models.Booking) { } templ workdayComponent(workDay *models.WorkDay) { - if len(workDay.Bookings) < 1 { + if workDay.IsEmpty() && !workDay.IsKurzArbeit() {Keine Buchung gefunden. Bitte Arbeitsstunden oder Grund der Abwesenheit eingeben!
} else { - if workDay.IsKurzArbeit() && len(workDay.Bookings) > 0 { + if workDay.IsKurzArbeit() { @absenceComponent(workDay.GetKurzArbeit(), true) } for _, booking := range workDay.Bookings {