diff --git a/Backend/endpoints/team.go b/Backend/endpoints/team.go index 30135da..843f899 100644 --- a/Backend/endpoints/team.go +++ b/Backend/endpoints/team.go @@ -1,15 +1,30 @@ package endpoints import ( + "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "arbeitszeitmessung/templates" + "context" + "errors" "log" "net/http" + "strconv" "time" ) func TeamHandler(w http.ResponseWriter, r *http.Request) { - showWeeks(w, r) + helper.RequiresLogin(Session, w, r) + switch r.Method { + case http.MethodPost: + submitReport(w, r) + break + case http.MethodGet: + showWeeks(w, r) + break + default: + http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) + break + } // user, err := (*models.User).GetUserFromSession(nil, Session, r.Context()) // if err != nil { // log.Println("No user found with the given personal number!") @@ -24,6 +39,39 @@ func TeamHandler(w http.ResponseWriter, r *http.Request) { // templates.TeamPage(teamMembers, userWorkDays).Render(r.Context(), w) } +func submitReport(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + log.Println("Error parsing form", err) + return + } + userPN, _ := strconv.Atoi(r.FormValue("user")) + _weekTs := r.FormValue("week") + weekTs, err := time.Parse(time.DateOnly, _weekTs) + user, err := (*models.User).GetByPersonalNummer(nil, userPN) + workWeek := (*models.WorkWeek).GetWeek(nil, user, weekTs, false) + + if err != nil { + log.Println("Could not get user!") + return + } + + switch r.FormValue("method") { + case "send": + err = workWeek.Send() + break + case "accept": + err = workWeek.Accept() + break + default: + break + } + if errors.Is(err, models.ErrRunningWeek) { + showWeeks(w, r.WithContext(context.WithValue(r.Context(), "error", true))) + } + showWeeks(w, r) +} + func showWeeks(w http.ResponseWriter, r *http.Request) { user, err := (*models.User).GetUserFromSession(nil, Session, r.Context()) if err != nil { @@ -37,16 +85,10 @@ func showWeeks(w http.ResponseWriter, r *http.Request) { weeks := (*models.WorkWeek).GetSendWeeks(nil, member) workWeeks = append(workWeeks, weeks...) } - // Somehow use this for the own users weeks - // lastSub := member.GetLastSubmission() - // weeks := getWeeksTillNow(lastSub) - // for _, week := range weeks { - // workWeek := (*models.WorkWeek).GetWeek(nil, member, week) - // workWeeks = append(workWeeks, workWeek) - // } lastSub := user.GetLastSubmission() - // userWorkDays := (*models.WorkDay).GetWorkDays(nil, user.CardUID, lastSub, lastSub.AddDate(0, 0, 7)) - userWeek := (*models.WorkWeek).GetWeek(nil, user, lastSub) + log.Println(lastSub) + userWeek := (*models.WorkWeek).GetWeek(nil, user, lastSub, true) + // isRunningWeek := time.Since(lastSub) < 24*5*time.Hour //the last submission is this week and cannot be send yet templates.TeamPage(workWeeks, userWeek).Render(r.Context(), w) } diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index a90458a..0e11a8e 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -14,17 +14,22 @@ import ( // Frontend relevant backend functionality -> not used by the arduino devices func TimeHandler(w http.ResponseWriter, r *http.Request) { + helper.RequiresLogin(Session, w, r) helper.SetCors(w) switch r.Method { - case "GET": + case http.MethodGet: getBookings(w, r) - case "POST": + break + case http.MethodPost: updateBooking(w, r) - case "OPTIONS": + break + case http.MethodOptions: // just support options header for non GET Requests from SWAGGER w.WriteHeader(http.StatusOK) + break default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) + break } } diff --git a/Backend/endpoints/time_create.go b/Backend/endpoints/time_create.go index e7e7c2e..449df4a 100644 --- a/Backend/endpoints/time_create.go +++ b/Backend/endpoints/time_create.go @@ -16,13 +16,17 @@ func TimeCreateHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPut: createBooking(w, r) + break case http.MethodGet: createBooking(w, r) + break case http.MethodOptions: // just support options header for non GET Requests from SWAGGER w.WriteHeader(http.StatusOK) + break default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + break } } diff --git a/Backend/endpoints/user.go b/Backend/endpoints/user.go index 7d8dafd..b225f39 100644 --- a/Backend/endpoints/user.go +++ b/Backend/endpoints/user.go @@ -1,6 +1,7 @@ package endpoints import ( + "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "arbeitszeitmessung/templates" "log" @@ -18,27 +19,26 @@ func CreateSessionManager(lifetime time.Duration) *scs.SessionManager { Session.Lifetime = lifetime return Session } + func LoginHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - showLoginForm(w, r, false) + showLoginPage(w, r, false) break case http.MethodPost: loginUser(w, r) break default: - showLoginForm(w, r, false) + http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) break } } func UserHandler(w http.ResponseWriter, r *http.Request) { - // if !Session.Exists(r.Context(), "user") { - // http.Redirect(w, r, "/user/login", http.StatusSeeOther) - // } + helper.RequiresLogin(Session, w, r) switch r.Method { case http.MethodGet: - showPWForm(w, r, 0) + showUserPage(w, r, 0) break case http.MethodPost: changePassword(w, r) @@ -49,7 +49,7 @@ func UserHandler(w http.ResponseWriter, r *http.Request) { } } -func showLoginForm(w http.ResponseWriter, r *http.Request, failed bool) { +func showLoginPage(w http.ResponseWriter, r *http.Request, failed bool) { templates.LoginPage(failed).Render(r.Context(), w) } @@ -85,10 +85,10 @@ func loginUser(w http.ResponseWriter, r *http.Request) { Session.Put(r.Context(), "user", user.PersonalNummer) http.Redirect(w, r, "/time", http.StatusSeeOther) //with this browser always uses GET } else { - showLoginForm(w, r, true) + showLoginPage(w, r, true) return } - showLoginForm(w, r, false) + showLoginPage(w, r, false) return } @@ -103,26 +103,26 @@ func changePassword(w http.ResponseWriter, r *http.Request) { password := r.FormValue("password") newPassword := r.FormValue("new_password") if password == "" || newPassword == "" || newPassword != r.FormValue("new_password_repeat") { - showPWForm(w, r, http.StatusBadRequest) + showUserPage(w, r, http.StatusBadRequest) return } user, err := (*models.User).GetByPersonalNummer(nil, Session.GetInt(r.Context(), "user")) if err != nil { log.Println("Error getting user!", err) - showPWForm(w, r, http.StatusBadRequest) + showUserPage(w, r, http.StatusBadRequest) } auth, err := user.ChangePass(password, newPassword) if err != nil { log.Println("Error when changing password!", err) } if auth { - showPWForm(w, r, http.StatusOK) + showUserPage(w, r, http.StatusOK) return } - showPWForm(w, r, http.StatusUnauthorized) + showUserPage(w, r, http.StatusUnauthorized) } -func showPWForm(w http.ResponseWriter, r *http.Request, status int) { +func showUserPage(w http.ResponseWriter, r *http.Request, status int) { templates.UserPage(status).Render(r.Context(), w) return } diff --git a/Backend/helper/web.go b/Backend/helper/web.go index 70a2288..4759f4d 100644 --- a/Backend/helper/web.go +++ b/Backend/helper/web.go @@ -3,6 +3,8 @@ package helper import ( "net/http" "os" + + "github.com/alexedwards/scs/v2" ) // setting cors, important for later frontend use @@ -15,3 +17,13 @@ func SetCors(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Headers", "*") } } + +func RequiresLogin(session *scs.SessionManager, w http.ResponseWriter, r *http.Request) { + if GetEnv("GO_ENV", "production") == "debug" { + return + } + if session.Exists(r.Context(), "user") { + return + } + http.Redirect(w, r, "/user/login", http.StatusSeeOther) +} diff --git a/Backend/main.go b/Backend/main.go index e1e88fb..0e40b50 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -4,12 +4,14 @@ import ( "arbeitszeitmessung/endpoints" "arbeitszeitmessung/helper" "arbeitszeitmessung/models" + "arbeitszeitmessung/templates" "context" "fmt" "log" "net/http" "time" + "github.com/a-h/templ" "github.com/joho/godotenv" _ "github.com/lib/pq" ) @@ -40,6 +42,7 @@ func main() { server.HandleFunc("/user/login", endpoints.LoginHandler) server.HandleFunc("/user", endpoints.UserHandler) server.HandleFunc("/team", endpoints.TeamHandler) + server.Handle("/", templ.Handler(templates.NavPage())) server.Handle("/static/", http.StripPrefix("/static/", fs)) serverSessionMiddleware := endpoints.Session.LoadAndSave(server) diff --git a/Backend/models/booking.go b/Backend/models/booking.go index 192fd34..65c24d4 100644 --- a/Backend/models/booking.go +++ b/Backend/models/booking.go @@ -116,11 +116,11 @@ func (b *Booking) GetBookingsByCardID(card_uid string, tsFrom time.Time, tsTo ti return bookings, nil } -func (b *Booking) GetBookingsGrouped(card_uid string, tsFrom time.Time, tsTo time.Time) ([]WorkDay, error){ +func (b *Booking) GetBookingsGrouped(card_uid string, tsFrom time.Time, tsTo time.Time) ([]WorkDay, error) { var grouped = make(map[string][]Booking) bookings, err := b.GetBookingsByCardID(card_uid, tsFrom, tsTo) - if (err != nil){ - log.Println("Failed to get bookings",err) + if err != nil { + log.Println("Failed to get bookings", err) return []WorkDay{}, nil } for _, booking := range bookings { @@ -161,14 +161,14 @@ func (b Booking) Save() { } func (b *Booking) GetBookingType() string { - switch b.CheckInOut{ - case 1,3: //manuelle Änderung + switch b.CheckInOut { + case 1, 3: //manuelle Änderung return "kommen" - case 2, 4: //manuelle Änderung + case 2, 4: //manuelle Änderung return "gehen" - case 255: + case 255: return "abgemeldet" - default: + default: return "Buchungs Typ unbekannt" } } @@ -183,7 +183,7 @@ func (b *Booking) Update(nb Booking) { if b.GeraetID != nb.GeraetID && nb.GeraetID != 0 { b.GeraetID = nb.GeraetID } - if(b.Timestamp != nb.Timestamp){ + if b.Timestamp != nb.Timestamp { b.Timestamp = nb.Timestamp } } @@ -209,19 +209,19 @@ func checkLastBooking(b Booking) bool { return true } -func (b *Booking) UpdateTime(newTime time.Time){ +func (b *Booking) UpdateTime(newTime time.Time) { hour, minute, _ := newTime.Clock() - if(hour == b.Timestamp.Hour() && minute == b.Timestamp.Minute()){ + if hour == b.Timestamp.Hour() && minute == b.Timestamp.Minute() { return } // TODO: add check for time overlap var newBooking Booking - newBooking.Timestamp = time.Date(b.Timestamp.Year(), b.Timestamp.Month(), b.Timestamp.Day(), hour, minute, 0, 0, time.Local) - if(b.CheckInOut < 3){ + newBooking.Timestamp = time.Date(b.Timestamp.Year(), b.Timestamp.Month(), b.Timestamp.Day(), hour, minute, 0, 0, time.Local) + if b.CheckInOut < 3 { newBooking.CheckInOut = b.CheckInOut + 2 } - if(b.CheckInOut == 255){ + if b.CheckInOut == 255 { newBooking.CheckInOut = 4 } b.Update(newBooking) diff --git a/Backend/models/user.go b/Backend/models/user.go index 3c0f0d0..94fd3d0 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -194,17 +194,38 @@ func parseUser(rows *sql.Rows) (User, error) { return user, nil } +// returns the start of the week, the last submission was made, submission == first booking or last send booking_report to team leader func (u *User) GetLastSubmission() time.Time { var lastSub time.Time - qStr, err := DB.Prepare("SELECT woche_start FROM buchung_wochen WHERE personal_nummer = $1 ORDER BY woche_start DESC LIMIT 1") + 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; +`) if err != nil { log.Println("Error preparing statement!", err) return lastSub } - err = qStr.QueryRow(u.PersonalNummer).Scan(&lastSub) + err = qStr.QueryRow(u.PersonalNummer, u.CardUID).Scan(&lastSub) if err != nil { log.Println("Error executing query!", err) return lastSub } + log.Println("From DB: ", lastSub) + lastSub = getMonday(lastSub) + lastSub = lastSub.Round(24 * time.Hour) + log.Println("After truncate: ", lastSub) return lastSub } + +func getMonday(ts time.Time) time.Time { + if ts.Weekday() != time.Monday { + if ts.Weekday() == time.Sunday { + ts = ts.AddDate(0, 0, -6) + } else { + ts = ts.AddDate(0, 0, -int(ts.Weekday()-1)) + } + } + return ts +} diff --git a/Backend/models/workWeek.go b/Backend/models/workWeek.go index 57da188..7d84436 100644 --- a/Backend/models/workWeek.go +++ b/Backend/models/workWeek.go @@ -1,27 +1,45 @@ package models import ( + "errors" "log" "time" ) type WorkWeek struct { + Id int WorkDays []WorkDay User User WeekStart time.Time + WorkHours time.Duration } -func (w *WorkWeek) GetWeek(user User, tsMonday time.Time) WorkWeek { +func (w *WorkWeek) GetWeek(user User, tsMonday time.Time, populateDays bool) WorkWeek { var week WorkWeek - week.WorkDays = (*WorkDay).GetWorkDays(nil, user.CardUID, tsMonday, tsMonday.Add(7*24*time.Hour)) + if populateDays { + week.WorkDays = (*WorkDay).GetWorkDays(nil, user.CardUID, tsMonday, tsMonday.Add(7*24*time.Hour)) + week.WorkHours = aggregateWorkTime(week.WorkDays) + } week.User = user week.WeekStart = tsMonday return week } +func (w *WorkWeek) GetWorkHourString() string { + return formatDuration(w.WorkHours) +} + +func aggregateWorkTime(days []WorkDay) time.Duration { + var workTime time.Duration + for _, day := range days { + workTime += day.workTime + } + return workTime +} + func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek { var weeks []WorkWeek - qStr, err := DB.Prepare(`SELECT woche_start::DATE FROM buchung_wochen WHERE bestaetigt = FALSE AND personal_nummer = $1;`) + qStr, err := DB.Prepare(`SELECT id, woche_start::DATE FROM wochen_report WHERE bestaetigt = FALSE AND personal_nummer = $1;`) if err != nil { log.Println("Error preparing SQL statement", err) return weeks @@ -37,11 +55,12 @@ func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek { for rows.Next() { var week WorkWeek week.User = user - if err := rows.Scan(&week.WeekStart); err != nil { + if err := rows.Scan(&week.Id, &week.WeekStart); err != nil { log.Println("Error scanning row!", err) return weeks } week.WorkDays = (*WorkDay).GetWorkDays(nil, user.CardUID, week.WeekStart, week.WeekStart.Add(7*24*time.Hour)) + week.WorkHours = aggregateWorkTime(week.WorkDays) weeks = append(weeks, week) } if err = rows.Err(); err != nil { @@ -50,3 +69,39 @@ func (w *WorkWeek) GetSendWeeks(user User) []WorkWeek { return weeks } + +var ErrRunningWeek = errors.New("Week is in running week") + +// creates a new entry in the woche_report table with the given workweek +func (w *WorkWeek) Send() error { + if time.Since(w.WeekStart) < 5*24*time.Hour { + log.Println("Cannot send week, because it's the running week!") + return ErrRunningWeek + } + qStr, err := DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start) VALUES ($1, $2);`) + if err != nil { + log.Println("Error preparing SQL statement", err) + return err + } + _, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart) + if err != nil { + log.Println("Error executing query!", err) + return err + } + return nil + +} + +func (w *WorkWeek) Accept() error { + qStr, err := DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = TRUE WHERE personal_nummer = $1 AND woche_start = $2;`) + if err != nil { + log.Println("Error preparing SQL statement", err) + return err + } + _, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart) + if err != nil { + log.Println("Error executing query!", err) + return err + } + return nil +} diff --git a/Backend/static/css/styles.css b/Backend/static/css/styles.css index 962e212..881cfaa 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -562,9 +562,6 @@ .mt-1 { margin-top: calc(var(--spacing) * 1); } - .-mb-1 { - margin-bottom: calc(var(--spacing) * -1); - } .mb-2 { margin-bottom: calc(var(--spacing) * 2); } @@ -666,9 +663,6 @@ .justify-center { justify-content: center; } - .justify-end { - justify-content: flex-end; - } .gap-2 { gap: calc(var(--spacing) * 2); } @@ -695,6 +689,11 @@ .justify-self-end { justify-self: flex-end; } + .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } .overflow-hidden { overflow: hidden; } @@ -777,6 +776,9 @@ .text-neutral-800 { color: var(--color-neutral-800); } + .text-red-500 { + color: var(--color-red-500); + } .text-red-600 { color: var(--color-red-600); } @@ -858,6 +860,13 @@ } } } + .hover\:text-accent { + &:hover { + @media (hover: hover) { + color: var(--color-accent); + } + } + } .hover\:text-white { &:hover { @media (hover: hover) { diff --git a/Backend/templates/pages.templ b/Backend/templates/pages.templ index 95ce082..6cea2c1 100644 --- a/Backend/templates/pages.templ +++ b/Backend/templates/pages.templ @@ -3,6 +3,8 @@ package templates import ( "arbeitszeitmessung/models" "fmt" + "strconv" + "time" ) templ Base() { @@ -69,30 +71,47 @@ templ UserPage(status int) { } -templ TeamPage(weeks []models.WorkWeek, week models.WorkWeek) { +templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) { {{ - year, kw := week.WeekStart.ISOWeek() + year, kw := userWeek.WeekStart.ISOWeek() }} @Base() @headerComponent()
Woche: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
an Vorgesetzten senden
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -275,7 +303,40 @@ func TeamPage(weeks []models.WorkWeek, week models.WorkWeek) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func NavPage() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_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 = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/teamComponents.templ b/Backend/templates/teamComponents.templ index 132142f..1ac4879 100644 --- a/Backend/templates/teamComponents.templ +++ b/Backend/templates/teamComponents.templ @@ -1,8 +1,11 @@ package templates -import "arbeitszeitmessung/models" - -import "fmt" +import ( + "arbeitszeitmessung/models" + "fmt" + "strconv" + "time" +) templ weekDayComponent(user models.User, day models.WorkDay) { {{ work, pause := day.GetWorkTimeString() }} @@ -26,18 +29,21 @@ templ employeComponent(week models.WorkWeek) {{ week.User.Vorname } { week.User.Name }
Arbeitszeit
-40h 12min
+{ week.GetWorkHourString() }
Arbeitszeit
40h 12min
Arbeitszeit
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(week.GetWorkHourString()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 32, Col: 52} + } + _, 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, "
Woche: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "