From 84def05c50bc17df8601baa83f0a95242071f305 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Sat, 8 Nov 2025 11:27:42 +0100 Subject: [PATCH] feat: change Manual time entry to work with hours instead of start and end time according to add hours as well the logic was changed to accept ours for manual entries instead of start and end time. This allows to add negative numbers as well, which are added to working time. --- backend/database.go | 102 ++++++++++++++++-------------------------- backend/handlers.go | 20 ++++----- backend/middleware.go | 1 - frontend/src/Main.elm | 96 +++++++++++++++------------------------ 4 files changed, 84 insertions(+), 135 deletions(-) diff --git a/backend/database.go b/backend/database.go index 6897bd9..be4fcb3 100644 --- a/backend/database.go +++ b/backend/database.go @@ -307,12 +307,12 @@ func GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) { year, week := t.ISOWeek() - var hours float64 - if entryType == "lesson" { - hours = 1.0 - } else { - hours = calculateHoursDiff(startTime, endTime) + entry := TimeEntry{ + Type: entryType, + StartTime: startTime, + EndTime: endTime, } + hours := calculateHours(entry) key := fmt.Sprintf("%d_%d_%d", userID, year, week) if existing, exists := hoursMap[key]; exists { @@ -359,57 +359,6 @@ func GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) { return result, nil } -// func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) { -// users, err := GetAllUsers(db) -// if err != nil { -// return nil, err -// } - -// entries, err := GetAllTimeEntries(db) -// if err != nil { -// return nil, err -// } - -// userTotals := make(map[int]float64) -// usernames := make(map[int]string) - -// for _, entry := range entries { -// var hours float64 -// if entry.Type == "lesson" { -// hours = 1.0 -// } else { -// hours = calculateHoursDiff(entry.StartTime, entry.EndTime) -// } -// userTotals[entry.UserID] += hours -// usernames[entry.UserID] = entry.Username -// } - -// var result []WeeklyHours -// for _, user := range users { -// if !user.IsAdmin { -// total := userTotals[user.ID] -// remaining := user.YearlyHours - total - -// result = append(result, WeeklyHours{ -// UserID: user.ID, -// Username: user.Username, -// Year: time.Now().Year(), -// Week: 0, -// TotalHours: total, -// YearlyTarget: user.YearlyHours, -// YearlyActual: total, -// RemainingYearly: remaining, -// }) -// } -// } - -// sort.Slice(result, func(i, j int) bool { -// return result[i].Username < result[j].Username -// }) - -// return result, nil -// } - func calculateHoursDiff(startTime, endTime string) float64 { parseTime := func(timeStr string) float64 { parts := strings.Split(timeStr, ":") @@ -471,7 +420,6 @@ func CheckUserHasEntriesForWeek(db *sql.DB, userID int, year int, week int) (boo var count int err := db.QueryRow(query, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4]).Scan(&count) - if err != nil { log.Printf("Error checking entries: %v", err) return false, err @@ -489,7 +437,7 @@ func GetActiveSchoolYear(db *sql.DB) (*SchoolYear, error) { `).Scan(&sy.ID, &sy.Name, &sy.StartDate, &sy.EndDate, &sy.IsActive, &sy.CreatedAt) if err == sql.ErrNoRows { - return nil, nil // Kein aktives Schuljahr + return nil, nil } return &sy, err } @@ -560,7 +508,6 @@ func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) { WHERE date >= ? AND date <= ? ORDER BY date DESC `, schoolYear.StartDate, schoolYear.EndDate) - if err != nil { return []WeeklyHours{}, err } @@ -576,12 +523,12 @@ func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) { continue } - var hours float64 - if entryType == "lesson" { - hours = 1.0 - } else { - hours = calculateHoursDiff(startTime, endTime) + entry := TimeEntry{ + Type: entryType, + StartTime: startTime, + EndTime: endTime, } + hours := calculateHours(entry) userTotals[userID] += hours } @@ -606,3 +553,30 @@ func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) { return result, nil } + +func CreateManualTimeEntry(db *sql.DB, entry *TimeEntry, hours float64) error { + entry.StartTime = fmt.Sprintf("%.2f", hours) + entry.EndTime = "manual" + entry.Type = "manual" + + _, err := db.Exec(` + INSERT INTO time_entries (user_id, schedule_id, date, type, start_time, end_time) + VALUES (?, 0, ?, ?, ?, ?) + `, entry.UserID, entry.Date, entry.Type, entry.StartTime, entry.EndTime) + + return err +} + +func calculateHours(entry TimeEntry) float64 { + if entry.Type == "lesson" { + return 1.0 + } else if entry.Type == "manual" { + hours, err := strconv.ParseFloat(entry.StartTime, 64) + if err != nil { + return 0 + } + return hours + } else { + return calculateHoursDiff(entry.StartTime, entry.EndTime) + } +} diff --git a/backend/handlers.go b/backend/handlers.go index e87b5f3..c6830fc 100644 --- a/backend/handlers.go +++ b/backend/handlers.go @@ -90,17 +90,15 @@ func (app *App) GetYearlyHoursSummaryHandler(c echo.Context) error { func (app *App) AdminCreateTimeEntryHandler(c echo.Context) error { isAdmin, _ := c.Get("is_admin").(bool) - if !isAdmin { return echo.NewHTTPError(http.StatusForbidden, "Only admins can create entries for others") } var req struct { - UserID int `json:"user_id"` - Date string `json:"date"` - StartTime string `json:"start_time"` - EndTime string `json:"end_time"` - Type string `json:"type"` + UserID int `json:"user_id"` + Date string `json:"date"` + Hours float64 `json:"hours"` + Type string `json:"type"` } if err := c.Bind(&req); err != nil { @@ -110,16 +108,16 @@ func (app *App) AdminCreateTimeEntryHandler(c echo.Context) error { entry := TimeEntry{ UserID: req.UserID, Date: req.Date, - StartTime: req.StartTime, - EndTime: req.EndTime, - Type: req.Type, + StartTime: "00:00", + EndTime: "00:00", + Type: "manual", } - if err := CreateTimeEntry(app.DB, &entry); err != nil { + if err := CreateManualTimeEntry(app.DB, &entry, req.Hours); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - return c.JSON(http.StatusCreated, map[string]string{"message": "time entry created"}) + return c.NoContent(http.StatusCreated) } func (app *App) GetUsersHandler(c echo.Context) error { diff --git a/backend/middleware.go b/backend/middleware.go index 857b9d0..4ee2231 100644 --- a/backend/middleware.go +++ b/backend/middleware.go @@ -192,7 +192,6 @@ func (l *LoginRateLimiter) Middleware() echo.MiddlewareFunc { func HTTPSRedirectMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - // Nur in Production aktivieren if os.Getenv("ENVIRONMENT") == "production" { if c.Request().Header.Get("X-Forwarded-Proto") != "https" { return c.Redirect(http.StatusMovedPermanently, diff --git a/frontend/src/Main.elm b/frontend/src/Main.elm index 4afe315..c19a812 100644 --- a/frontend/src/Main.elm +++ b/frontend/src/Main.elm @@ -220,8 +220,7 @@ type alias YearlyHoursSummary = type alias AdminManualEntry = { selectedUserId : Maybe Int , date : String - , startTime : String - , endTime : String + , hours : String , entryType : String } @@ -293,7 +292,7 @@ init flags = , userPasswordInput = "" , isProcessing = False , mobileMenuOpen = False - , adminManualEntryForm = AdminManualEntry Nothing "" "" "" "lesson" + , adminManualEntryForm = AdminManualEntry Nothing "" "" "manual" , schoolYears = [] , newSchoolYear = NewSchoolYear "" "" "" , activeSchoolYear = Nothing @@ -412,8 +411,7 @@ type Msg | CloseMobileMenu | SelectUserForManualEntry Int | UpdateManualEntryDate String - | UpdateManualEntryStartTime String - | UpdateManualEntryEndTime String + | UpdateManualEntryHours String | UpdateManualEntryType String | SaveAdminTimeEntry | AdminTimeEntrySaved (Result Http.Error ()) @@ -1437,19 +1435,12 @@ update msg model = in ( { model | adminManualEntryForm = { form | date = date } }, Cmd.none ) - UpdateManualEntryStartTime time -> + UpdateManualEntryHours hours -> let form = model.adminManualEntryForm in - ( { model | adminManualEntryForm = { form | startTime = time } }, Cmd.none ) - - UpdateManualEntryEndTime time -> - let - form = - model.adminManualEntryForm - in - ( { model | adminManualEntryForm = { form | endTime = time } }, Cmd.none ) + ( { model | adminManualEntryForm = { form | hours = hours } }, Cmd.none ) UpdateManualEntryType entryType -> let @@ -1470,7 +1461,7 @@ update msg model = case model.token of Just token -> ( { model - | adminManualEntryForm = AdminManualEntry Nothing "" "" "" "lesson" + | adminManualEntryForm = AdminManualEntry Nothing "" "" "manual" , error = Nothing , isProcessing = False } @@ -1997,6 +1988,14 @@ calculateHours startTime endTime = if end > start then end - start + else if endTime == "manual" then + case String.toFloat startTime of + Just time -> + time + + Nothing -> + 0 + else 0 @@ -2661,23 +2660,28 @@ viewYearlyHourRow summary = viewAdminManualEntryForm : Model -> Html Msg viewAdminManualEntryForm model = div [ class "box has-background-info-light" ] - [ h3 [ class "subtitle" ] [ text "Neuer Zeiteintrag" ] + [ h3 [ class "subtitle" ] [ text "Manuelle Stundeneintragung" ] + , p [ class "help mb-3" ] + [ text "Positive Werte = Abzug, Negative Werte = Hinzurechnung" ] , div [ class "columns" ] - [ div [ class "column" ] + [ div [ class "column is-4" ] [ div [ class "field" ] [ label [ class "label" ] [ text "Mitarbeiter" ] , div [ class "control" ] [ div [ class "select is-fullwidth" ] [ select [ onInput (SelectUserForManualEntry << Maybe.withDefault 0 << String.toInt) ] - (option [] [ text "-- Wählen --" ] - :: List.map (\user -> option [ value (String.fromInt user.id) ] [ text user.username ]) - (List.filter (\u -> not u.isAdmin) model.users) + (option [ value "" ] [ text "-- Wählen --" ] + :: List.map + (\u -> + option [ value (String.fromInt u.id), selected (model.adminManualEntryForm.selectedUserId == Just u.id) ] [ text u.username ] + ) + model.users ) ] ] ] ] - , div [ class "column" ] + , div [ class "column is-4" ] [ div [ class "field" ] [ label [ class "label" ] [ text "Datum" ] , div [ class "control" ] @@ -2691,47 +2695,22 @@ viewAdminManualEntryForm model = ] ] ] - ] - , div [ class "columns" ] - [ div [ class "column" ] + , div [ class "column is-4" ] [ div [ class "field" ] - [ label [ class "label" ] [ text "Startzeit" ] + [ label [ class "label" ] [ text "Stunden (z.B. 2.5 oder -1.0)" ] , div [ class "control" ] [ input [ class "input" - , type_ "time" - , value model.adminManualEntryForm.startTime - , onInput UpdateManualEntryStartTime + , type_ "number" + , step "0.5" + , placeholder "z.B. 2.5 oder -1.0" + , value model.adminManualEntryForm.hours + , onInput UpdateManualEntryHours ] [] ] - ] - ] - , div [ class "column" ] - [ div [ class "field" ] - [ label [ class "label" ] [ text "Endzeit" ] - , div [ class "control" ] - [ input - [ class "input" - , type_ "time" - , value model.adminManualEntryForm.endTime - , onInput UpdateManualEntryEndTime - ] - [] - ] - ] - ] - , div [ class "column" ] - [ div [ class "field" ] - [ label [ class "label" ] [ text "Typ" ] - , div [ class "control" ] - [ div [ class "select is-fullwidth" ] - [ select [ onInput UpdateManualEntryType, value model.adminManualEntryForm.entryType ] - [ option [ value "lesson" ] [ text "Unterricht" ] - , option [ value "break" ] [ text "Pause" ] - ] - ] - ] + , p [ class "help" ] + [ text "Positiv: Wird abgezogen | Negativ: Wird hinzugerechnet" ] ] ] ] @@ -2743,7 +2722,7 @@ viewAdminManualEntryForm model = , disabled (case model.adminManualEntryForm.selectedUserId of Just _ -> - model.isProcessing + model.isProcessing || String.isEmpty model.adminManualEntryForm.hours Nothing -> True @@ -4012,9 +3991,8 @@ createAdminTimeEntry token entry = Encode.object [ ( "user_id", Encode.int userId ) , ( "date", Encode.string entry.date ) - , ( "start_time", Encode.string entry.startTime ) - , ( "end_time", Encode.string entry.endTime ) - , ( "type", Encode.string entry.entryType ) + , ( "hours", Encode.float (String.toFloat entry.hours |> Maybe.withDefault 0) ) + , ( "type", Encode.string "manual" ) ] , expect = Http.expectWhatever AdminTimeEntrySaved , timeout = Nothing