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